wam-community
Version:
A collection of prebuilt Web Audio Modules ready for use
834 lines (698 loc) • 22.9 kB
JavaScript
// https://github.com/g200kg/webaudio-controls/blob/master/webaudio-controls.js
import "../../utils/webaudio-controls.js";
// This works when youuse a bundler such as rollup
// If you do no wan to use a bundler, then look at other examples
// that build in pure JS the syles and html template directly
// in the code...
let style = `
-face {
font-family: 'smallFont';
src: url('./assets/fonts/secrcode-Regular.ttf') format('truetype');
font-weight: normal;
font-style: normal;
}
:host {
background: linear-gradient(rgb(114, 70, 155), rgb(44, 34, 109));
box-shadow: 4px 5px 6px rgba(0, 0, 0, 0.7), inset -2px -2px 5px 0px rgba(0, 0, 0, 0.2), inset 3px 1px 1px 4px rgba(255, 255, 255, 0.2), 1px 0px 1px 0px rgba(0, 0, 0, 0.9), 0 2px 1px 0 rgba(0, 0, 0, 0.9), 1px 1px 1px 0px rgba(0, 0, 0, 0.9);
width: 480px;
height: 350px;
display: block;
user-select: none;
cursor: move;
z-index: 9;
border-bottom-left-radius: 8px;
border-bottom-right-radius: 8px;
}
.pedalLabel,
.div_buttons {
border-radius: 8px;
}
.div_menuPanel {
/* border-top-left-radius: 8px;
border-top-right-radius: 8px; */
}
webaudio-switch {
left: -6px;
}
span {
font-family: helvetica;
text-shadow: 0px 1px 1px #000;
}
.pedalLabel {
/* background: rgba(0, 0, 0, 0.4); */
box-shadow: inset 0px 1px 5px #111;
border: 1px solid #ccc;
color: rgb(204, 204, 204);
padding: 4px 10px;
font-size: 10px;
text-align: center;
user-select: none;
font-family: helvetica;
text-transform: uppercase;
margin-top: 10px;
}
.knob-label {
color: rgb(255, 255, 255);
text-shadow: 0px 1px 1px #000;
font-size: 9px;
margin-top: -2px;
text-align: center;
text-transform: uppercase;
overflow: hidden;
user-select: none;
font-family: helvetica;
margin-top: 5px;
}
.elements {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
padding: 10px;
}
.column {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
flex: 1;
margin: 5px;
margin-top: 10px;
border-radius: 10px;
z-index: 0;
}
.frequencyMeasure {
background-color: #000
}
.row {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: stretch;
}`;
let template = `
<div id="wc-equalizer" class="wrapper">
<div class="row">
<div class="column">
<div id="DivFilterBank"></div>
<span class="knob-label" id="knob1-label">EFFECT</span>
<webaudio-switch id="switch1" class="switch" midilearn="true" src="./assets/switches/switch_1.png"
width="32" height="20"></webaudio-switch>
<span class="pedalLabel">Equalizer</span>
</div>
</div>
</div>
`;
let switchImg = "./assets/switches/switch_1.png";
const getAssetUrl = (asset) => {
const base = new URL(".", import.meta.url);
return `${base}${asset}`;
};
// The GUI is a WebComponent. Not mandatory but useful.
// MANDORY : the GUI should be a DOM node. WebComponents are
// practical as they encapsulate everyhing in a shadow dom
export default class GraphicEQHTMLElement extends HTMLElement {
// plugin = the same that is passed in the DSP part. It's the instance
// of the class that extends WebAudioModule. It's an Observable plugin
constructor(plugin) {
super();
this.root = this.attachShadow({ mode: "open" });
this.root.innerHTML = `<style>${style}</style>${template}`;
// MANDATORY for the GUI to observe the plugin state
this.plugin = plugin;
this.setResources();
this.setKnobs();
this.setSwitchListener();
this.gridColor = "rgb(235,235,235)";
this.textColor = "rgb(235, 235, 235)";
this.nyquist = 0.5 * this.plugin.audioContext.sampleRate;
this.noctaves = 11;
this.setSwitchListener();
this.setCanvas();
this.draw();
window.requestAnimationFrame(this.handleAnimationFrame);
}
get properties() {
this.boundingRect = {
dataWidth: {
type: Number,
value: 480,
},
dataHeight: {
type: Number,
value: 350,
},
};
return this.boundingRect;
}
updateStatus = (status) => {
this.shadowRoot.querySelector("#switch1").value = status;
};
handleAnimationFrame = () => {
/*
const {
lowGain,
midLowGain,
midHighGain,
highGain,
enabled,
} = this.plugin.audioNode.getParamsValues();
this.shadowRoot.querySelector('#knob1').value = lowGain;
this.shadowRoot.querySelector('#knob2').value = midLowGain;
this.shadowRoot.querySelector('#knob3').value = midHighGain;
this.shadowRoot.querySelector('#knob4').value = highGain;
this.shadowRoot.querySelector('#switch1').value = enabled;
*/
this.draw();
window.requestAnimationFrame(this.handleAnimationFrame);
};
/**
* Change relative URLS to absolute URLs for CSS assets, webaudio controls spritesheets etc.
*/
setResources() {
// Setting up the switches imgs, those are loaded from the assets
this.root.querySelector("webaudio-switch").setAttribute("src", getAssetUrl(switchImg));
}
setKnobs() {}
setSwitchListener() {
console.log("GraphicEQ : set switch listener");
const { plugin } = this;
// by default, plugin is disabled
plugin.audioNode.setParamsValues({ enabled: 1 });
this.shadowRoot.querySelector("#switch1").addEventListener("change", function onChange() {
plugin.audioNode.setParamsValues({ enabled: +!!this.checked });
});
}
setCanvas() {
// canvas
this.canvas = document.createElement("canvas");
// get dimensions, by default the ones from the parent element
this.canvas.classList.add("graphicEQ");
this.canvas.width = this.width = 430;
this.canvas.height = this.height = 250;
this.canvas.style.position = "absolute";
this.ctx = this.canvas.getContext("2d");
this.canvasParent = this.root.querySelector("#DivFilterBank");
this.canvasParent.appendChild(this.canvas);
this.canvas2 = document.createElement("canvas");
this.canvas2.classList.add("frequencyMeasure");
this.canvas2.style.position = "relative";
this.canvas2.style.top = 0;
this.canvas2.style.left = 0;
this.canvas2.width = this.canvas.width;
this.canvas2.height = this.canvas.height;
this.canvas2.style.zIndex = -1;
this.canvasParent.appendChild(this.canvas2);
this.ctx2 = this.canvas2.getContext("2d");
this.dbScale = 60;
//TODO:
this.pixelsPerDb = (0.5 * this.height) / this.dbScale;
// listeners
this.canvas.addEventListener("mousemove", this.processMouseMove.bind(this));
this.canvas.addEventListener("mousedown", this.processMouseDown.bind(this));
this.canvas.addEventListener("mouseup", this.processMouseUp.bind(this));
requestAnimationFrame(this.drawFrequencies.bind(this));
this.canvas.addEventListener("mouseenter", this.setFocus, false);
this.canvas.addEventListener("mouseleave", this.unsetFocus, false);
/*window.addEventListener('resize',
(evt) => {
console.log("resize canvasParent:" + this.canvasParent.clientWidth + " " + this.canvasParent.clientHeight);
this.resize(this.canvasParent.clientWidth,
this.canvasParent.clientHeight);
});*/
}
setFocus() {
this.focus();
this.tabIndex = 1;
}
unsetFocus() {
this.blur();
this.tabIndex = -1;
}
draw() {
let ctx = this.ctx;
ctx.save();
ctx.clearRect(0, 0, this.width, this.height);
this.sumCurveParallel = []; // curves of (x, y) pixels thats sums all filter curves
this.sumCurveSerie = [];
for (let x = 0; x < this.width; x++) {
this.sumCurveParallel[x] = 0;
this.sumCurveSerie[x] = 0;
}
this.plugin.audioNode.filters.forEach((filt) => {
this.drawFilterCurves(filt);
// draw control point
this.drawControlPoint(filt);
});
this.drawGrid();
if (this.plugin.audioNode.filters.length > 0) {
//this.drawSumOfAllFilterCurveParallel();
this.drawSumOfAllFilterCurveSerie();
}
if (this.mode === "dragControlPoint") this.drawTooltip();
ctx.restore();
}
//TODO: how to manipulate this function about the zoom of pedalboard
resize(w, h) {
//console.log("resize");
let r = this.canvasParent.getBoundingClientRect();
let rc = this.canvas.getBoundingClientRect();
//this.canvas.width = this.width = this.canvasParent.clientWidth - (r.left + rc.left) / 2;
//this.canvas.height = this.height = this.canvasParent.clientHeight - (r.top + rc.top) / 2;
this.canvas.width = this.width = window.innerWidth;
this.canvas.heigth = this.height = window.innerHeight;
this.canvas2.width = this.canvas.width;
this.canvas2.height = this.canvas.height;
this.pixelsPerDb = (0.5 * this.height) / this.dbScale;
//this.clearCanvas();
this.draw();
}
drawFilterCurves(filt) {
let frequencyHz = new Float32Array(this.canvas.width);
let magResponse = new Float32Array(this.canvas.width);
let phaseResponse = new Float32Array(this.canvas.width);
let ctx = this.ctx;
ctx.save();
ctx.globalAlpha = 0.3;
ctx.strokeStyle = "white";
ctx.fillStyle = filt.color;
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(0, 0);
// First get response.
for (let i = 0; i < this.width; i++) {
let f = i / this.width;
// Convert to log frequency scale (octaves).
f = this.nyquist * Math.pow(2.0, this.noctaves * (f - 1.0));
frequencyHz[i] = f;
}
filt.getFrequencyResponse(frequencyHz, magResponse, phaseResponse);
/*
var magResponseOfFilter = new Float32Array(width);
filt.getFrequencyResponse(frequencyHz, magResponseOfFilter, phaseResponse);
// sum it to magResponse
for(var l = 0; l < width; l++) {
magResponse[l] = magResponseOfFilter[l];
}
*/
// Draw filters curves
for (let i = 0; i < this.width; i++) {
let f = magResponse[i];
let response = magResponse[i];
let dbResponse = 2 * ((10.0 * Math.log(response)) / Math.LN10);
let x = i;
let y = this.dbToY(dbResponse);
// curve that sums all filter responses
this.sumCurveParallel[x] += response;
this.sumCurveSerie[x] += dbResponse;
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}
//ctx.lineTo(this.width, this.height);
ctx.lineTo(this.width, this.height / 2);
ctx.lineTo(0, this.height / 2);
ctx.fill();
//ctx.stroke();
ctx.restore();
}
drawControlPoint(filter) {
let q = filter.Q.value; // unit = db
let f = filter.frequency.value;
let gain = filter.gain.value;
let x, y;
//TODO: See this function to know X
x = this.fToX(f);
switch (filter.type) {
case "lowpass":
case "highpass":
y = this.dbToY(q);
break;
case "lowshelf":
case "highshelf":
y = this.dbToY(gain / 2);
break;
case "peaking":
y = this.dbToY(gain);
break;
case "notch":
case "bandpass":
if (q > 0.16) {
y = this.dbToY(q);
}
break;
}
filter.controlPoint = {
x: x,
y: y,
};
this.drawControlPointAsColoredCircle(x, y, filter.color);
}
drawControlPointAsColoredCircle(x, y, color) {
let ctx = this.ctx;
ctx.save();
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(x + 1, y, 5, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
getFontSizeDependingOnWidth() {
return this.width / 45;
}
getFontSizeDependingOnHeight() {
return this.height / 35;
}
drawGrid() {
let ctx = this.ctx;
ctx.save();
let fontSize = this.getFontSizeDependingOnWidth();
ctx.beginPath();
ctx.lineWidth = 1;
ctx.font = "300 " + fontSize + "px OpenSans, sans-serif";
// Draw frequency scale.
for (let octave = 0; octave + 1 <= this.noctaves; octave++) {
let x = (octave * this.width) / this.noctaves;
ctx.strokeStyle = this.gridColor;
ctx.moveTo(x, 30);
ctx.lineTo(x, this.height);
ctx.stroke();
let f = this.nyquist * Math.pow(2.0, octave - this.noctaves);
let value = f.toFixed(0);
let unit = "Hz";
if (f > 1000) {
unit = "KHz";
value = (f / 1000).toFixed(1);
}
ctx.textAlign = "center";
ctx.fillStyle = this.textColor;
if (octave !== 0) ctx.fillText(value + unit, x, 20);
else ctx.fillText(value + unit, 10, 20);
}
// Draw 0dB line.
fontSize = this.getFontSizeDependingOnHeight();
ctx.font = "300 " + fontSize + "px OpenSans, sans-serif";
ctx.beginPath();
ctx.moveTo(0, 0.5 * this.height);
ctx.lineTo(this.width, 0.5 * this.height);
ctx.stroke();
// Draw decibel scale.
let dbIncrement = (this.dbScale * 10) / 60;
for (let db = -this.dbScale; db + dbIncrement < this.dbScale; db += dbIncrement) {
let y = this.dbToY(db);
ctx.fillStyle = this.textColor;
ctx.fillText(db.toFixed(0) + "dB", this.width - this.width / 25, y - 2);
ctx.strokeStyle = this.gridColor;
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(this.width, y);
ctx.stroke();
}
ctx.restore();
}
drawSumOfAllFilterCurveSerie() {
//console.log("draw sum")
let ctx = this.ctx;
ctx.save();
ctx.strokeStyle = "white";
ctx.beginPath();
for (let x = 0; x < this.width; x++) {
//let y = this.dbToY(0) -
let dbResponse = this.sumCurveSerie[x];
let y = this.dbToY(dbResponse);
//drawControlPointAsColoredCircle(x, y, "red");
if (y !== NaN) {
if (x === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}
//console.log("x = " + x + " y = " + y)
}
ctx.stroke();
ctx.restore();
}
map(value, istart, istop, ostart, ostop) {
return ostart + (ostop - ostart) * ((value - istart) / (istop - istart));
}
fToX(f) {
// logarithmic scale
var logf = Math.log2(f);
var logmaxf = Math.log2(this.nyquist); // 24Khz for 11 octaves at 48Khz
var logminf = Math.log2(10); // min freq value in our graphic
var x = this.map(logf, logminf, logmaxf, -this.width / 50, this.width);
return x;
}
dbToY(db) {
var y = 0.5 * this.height - this.pixelsPerDb * db;
return y;
}
processMouseMove(e) {
this.mousePos = this.getMousePos(e);
e.stopPropagation();
//console.log(this.mousePos);
//console.log(this.canvasParent.clientWidth);
switch (this.mode) {
case "none":
// color each control point in red
this.plugin.audioNode.filters.forEach((f) => {
this.drawControlPointAsColoredCircle(f.controlPoint.x, f.controlPoint.y, f.color);
});
// if mouse close to a control point color it in green
if ((this.selectedFilter = this.findClosestFilterControlPoint(this.mousePos.x, this.mousePos.y))) {
this.drawControlPointAsColoredCircle(
this.selectedFilter.controlPoint.x,
this.selectedFilter.controlPoint.y,
"red"
);
}
break;
case "dragControlPoint":
this.dy = this.clickedY - this.mousePos.y;
const scale = this.getBoundingClientRect().width / this.offsetWidth;
this.changeFilterParameters(
this.selectedFilter,
this.mousePos.x / scale,
this.mousePos.y / scale,
this.shiftPressed,
this.dy
);
break;
}
return false;
}
processMouseDown(e) {
e.stopPropagation();
e.preventDefault();
if (this.selectedFilter) {
this.selectedFilter.originalQValue = this.selectedFilter.Q.value;
this.mode = "dragControlPoint";
this.shiftPressed = e.shiftKey;
this.clickedY = this.getMousePos(e).y;
this.drawTooltip();
}
// console.log("mode=START DRAGGING");
}
processMouseUp(e) {
this.mode = "none";
this.selectedFilter = null;
this.shiftPressed = false;
this.dy = 0;
this.draw();
}
drawFrequencies() {
//clear the canvas
this.ctx2.clearRect(0, 0, this.canvas2.width, this.canvas2.height);
// Get analyser data
this.plugin.audioNode.analyser.getFloatFrequencyData(this.plugin.audioNode.dataArray);
var barWidth = [];
var barHeight;
var x = 0;
var analyserRange = this.plugin.audioNode.analyser.maxDecibels - this.plugin.audioNode.analyser.minDecibels;
// ration between analyser range and our range
var range = this.dbScale * 2;
var dbRatio = range / analyserRange;
for (var i = 0; i < this.plugin.audioNode.bufferLength; i++) {
// values are all negative
barHeight = this.dbToY(-this.plugin.audioNode.dataArray[i]);
barHeight *= dbRatio;
//this.ctx2.fillStyle = 'rgb(' + (barHeight+100) + ',0,0)';
this.ctx2.fillStyle = "red";
barWidth[i] =
((Math.log(i + 2) - Math.log(i + 1)) * this.canvas2.width) / Math.log(this.plugin.audioNode.bufferLength - 80);
this.ctx2.fillRect(x, this.canvas2.height - barHeight, barWidth[i], barHeight);
// 2 is the number of pixels between bars
x += barWidth[i] + 1;
}
// call again the visualize function at 60 frames/s
requestAnimationFrame(this.drawFrequencies.bind(this));
}
getMousePos(e) {
let rect = this.canvas.getBoundingClientRect();
//console.log("cx= " + e.clientX + " lx= " + e.layerX);
let mouseX = e.clientX - rect.left;
let mouseY = e.clientY - rect.top;
return {
x: mouseX,
y: mouseY,
};
}
findClosestFilterControlPoint(x, y) {
for (let i = 0; i < this.plugin.audioNode.filters.length; i++) {
let f = this.plugin.audioNode.filters[i];
let cp = f.controlPoint;
const scale = this.getBoundingClientRect().width / this.offsetWidth;
if (this.distance(x / scale, y / scale, cp.x, cp.y) < 5 / scale) {
// mouse cursor is over a control point
return f;
}
//console.log(x, y);
//console.log(cp.x, cp.y);
}
return null;
}
dyToQ(dy) {
let q;
if (dy < 0) {
q = this.map(dy, 0, -100, this.selectedFilter.originalQValue, 0.1);
} else {
q = this.map(dy, 0, 100, this.selectedFilter.originalQValue, 15);
}
return q;
}
distance(x1, y1, x2, y2) {
let dx = x1 - x2;
let dy = y1 - y2;
return Math.sqrt(dx * dx + dy * dy);
}
drawTooltip() {
if (this.mousePos === undefined) return;
let tooltipWidth = 50,
tooltipHeight;
let displayQ = false,
displayGain = false;
switch (this.selectedFilter.type) {
case "lowpass":
case "highpass":
case "bandpass":
case "notch":
displayQ = true;
displayGain = false;
tooltipHeight = 44;
break;
case "peaking":
displayQ = true;
displayGain = true;
tooltipHeight = 55;
break;
case "highshelf":
case "lowshelf":
displayQ = false;
displayGain = true;
tooltipHeight = 44;
break;
}
let ctx = this.ctx;
ctx.save();
if (this.mousePos.x < this.canvas.width - tooltipWidth) ctx.translate(this.mousePos.x, this.mousePos.y);
else ctx.translate(this.mousePos.x - tooltipWidth, this.mousePos.y);
ctx.fillStyle = "rgba(200, 200, 200, 0.3)";
ctx.strokeStyle = "rgba(153, 153, 153)";
ctx.beginPath();
ctx.rect(10, 0, tooltipWidth, tooltipHeight);
ctx.fill();
ctx.stroke();
ctx.fillStyle = "rgba(255, 255, 255, 0.7)";
ctx.font = "11px OpenSans, sanserif";
let xText = 12;
ctx.fillText(this.selectedFilter.type, 15, xText);
xText += 13;
ctx.fillText(this.selectedFilter.frequency.value.toFixed(0) + "Hz", 15, xText);
xText += 13;
if (displayGain) {
ctx.fillText(this.selectedFilter.gain.value.toFixed(0) + "dB/Oct", 15, 38);
xText += 13;
}
if (displayQ) ctx.fillText(this.selectedFilter.Q.value.toFixed(3), 15, xText);
ctx.restore();
}
changeFilterParameters(filter, x, y, shiftPressed, dy) {
// x = frequency, y = Db
let db = this.yToDb(y);
let f = this.xToF(x);
//console.log("f = " + f + " db = " + db);
const index = this.plugin.audioNode.filters.indexOf(filter);
this.plugin.audioNode.setParamValue(`${filter.type}_${index}_frequency`, f);
// filter.frequency.value = f;
switch (filter.type) {
case "lowpass":
case "highpass":
// Here important! Do not code with this.plugin.audioNode.Q = but instead use the exposed param names
// and setParamValue from CompositeAudioNode if you want getState/setState + automation to work
this.plugin.audioNode.setParamValue(`${filter.type}_${index}_Q`, db);
// filter.Q.value = db;
this.draw();
break;
case "lowshelf":
case "highshelf":
this.plugin.audioNode.setParamValue(`${filter.type}_${index}_gain`, 2 * db);
// filter.gain.value = 2 * db;
this.draw();
break;
case "peaking":
if (!shiftPressed) this.plugin.audioNode.setParamValue(`${filter.type}_${index}_gain`, db);
// filter.gain.value = db;
else {
this.plugin.audioNode.setParamValue(`${filter.type}_${index}_Q`, this.dyToQ(dy));
// filter.Q.value = this.dyToQ(dy);
}
this.draw();
break;
case "notch":
if (!shiftPressed) this.plugin.audioNode.setParamValue(`${filter.type}_${index}_gain`, db);
// filter.gain.value = db;
else {
this.plugin.audioNode.setParamValue(`${filter.type}_${index}_Q`, this.dyToQ(dy));
// filter.Q.value = this.dyToQ(dy);
}
this.draw();
break;
case "bandpass":
if (db >= 0.16) this.plugin.audioNode.setParamValue(`${filter.type}_${index}_Q`, db);
// filter.Q.value = db;
this.draw();
break;
}
}
yToDb(y) {
var db = (0.5 * this.height - y) / this.pixelsPerDb;
return db;
}
xToF(x) {
// x corresponds to a freq in log scale
// logarithmic scale
var logmaxf = Math.log2(this.nyquist); // 24Khz for 11 octaves at 48Khz
var logminf = Math.log2(10); // min freq value in our graphic
var flog = this.map(x, -this.width / 50, this.width, logminf, logmaxf);
return Math.pow(2, flog); // reverse of a log function is 2^logf
}
/*
bypass() {
this.plugin.params.state = "disable";
this.plugin.audioNode.connectNodes();
}
reactivate() {
this.plugin.params.state = "enable";
this.plugin.audioNode.connectNodes();
}
*/
letDrag() {
this.root.querySelector("#DivFilterBank").draggable = true;
}
// name of the custom HTML element associated
// with the plugin. Will appear in the DOM if
// the plugin is visible
static is() {
return "wasabi-graphic-eq";
}
}
if (!customElements.get(GraphicEQHTMLElement.is())) {
customElements.define(GraphicEQHTMLElement.is(), GraphicEQHTMLElement);
}