Skip to content

Commit 4740939

Browse files
committed
fix(Equalizer): Labels Added
1 parent 82863f7 commit 4740939

File tree

3 files changed

+188
-91
lines changed

3 files changed

+188
-91
lines changed

src/components/Equalizers.jsx

Lines changed: 32 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState } from 'react';
1+
import { useState, useMemo } from 'react';
22
import Display from './Display';
33

44
/**
@@ -25,6 +25,36 @@ function EQ({ wasmModule, width, height, freqs: liveFreqs }) {
2525
const [nodes, setNodes] = useState(initialNodes);
2626
const [curves, setCurves] = useState(initialCurves);
2727

28+
const processedFreqs = useMemo(() => {
29+
if (!wasmModule || !liveFreqs) return [];
30+
31+
let nodesVec, curvesVec, freqsVec;
32+
try {
33+
// Manually convert JS arrays to the Embind Vector types.
34+
nodesVec = new wasmModule.VectorNode();
35+
nodes.forEach(node => nodesVec.push_back(node));
36+
37+
curvesVec = new wasmModule.VectorDouble();
38+
curves.forEach(curve => curvesVec.push_back(curve));
39+
40+
freqsVec = new wasmModule.VectorVectorDouble();
41+
(liveFreqs || []).forEach(freqPair => {
42+
const pair = new wasmModule.VectorDouble();
43+
pair.push_back(freqPair[0]);
44+
pair.push_back(freqPair[1]);
45+
freqsVec.push_back(pair);
46+
pair.delete();
47+
});
48+
49+
return wasmModule.applyEnvelope(nodesVec, curvesVec, freqsVec);
50+
} finally {
51+
// Ensure memory is always freed, even if an error occurs.
52+
if (nodesVec) nodesVec.delete();
53+
if (curvesVec) curvesVec.delete();
54+
if (freqsVec) freqsVec.delete();
55+
}
56+
}, [wasmModule, nodes, curves, liveFreqs]);
57+
2858
return (
2959
<div className='EQ'>
3060
<h3>Frequency EQ</h3>
@@ -36,32 +66,7 @@ function EQ({ wasmModule, width, height, freqs: liveFreqs }) {
3666
curves={curves}
3767
onNodesChange={setNodes}
3868
onCurvesChange={setCurves}
39-
freqs={wasmModule ? (() => {
40-
// Manually convert JS arrays to the Embind Vector types.
41-
const nodesVec = new wasmModule.VectorNode();
42-
nodes.forEach(node => nodesVec.push_back(node));
43-
44-
const curvesVec = new wasmModule.VectorDouble();
45-
curves.forEach(curve => curvesVec.push_back(curve));
46-
47-
const freqsVec = new wasmModule.VectorVectorDouble();
48-
(liveFreqs || []).forEach(freqPair => {
49-
const pair = new wasmModule.VectorDouble();
50-
pair.push_back(freqPair[0]);
51-
pair.push_back(freqPair[1]);
52-
freqsVec.push_back(pair);
53-
pair.delete();
54-
});
55-
56-
const result = wasmModule.applyEnvelope(nodesVec, curvesVec, freqsVec);
57-
58-
// Clean up the memory allocated by Embind
59-
nodesVec.delete();
60-
curvesVec.delete();
61-
freqsVec.delete();
62-
63-
return result;
64-
})() : []}
69+
freqs={processedFreqs}
6570
isLogarithmic={true}
6671
wasmModule={wasmModule}
6772
/>

src/hooks/useCanvasDrawing.js

Lines changed: 155 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useEffect, useMemo } from 'react';
1+
import { useEffect, useMemo, useCallback } from 'react';
22

33
const style = {
44
backgroundColor: "#0f172a",
@@ -7,76 +7,146 @@ const style = {
77
connectorColor: "#ffffff",
88
connectorWidth: 2,
99
nodeRadius: 6,
10+
axisColor: "#999",
11+
textColor: "#fff",
12+
gridColor: "rgba(255,255,255,0.1)"
1013
};
1114

12-
const useCanvasDrawing = (canvasRef, { wasmModule, width, height, nodes, xRange, curves, freqs, isLogarithmic, onWheel }) => {
13-
useEffect(() => {
14-
if (!width || !height) return;
15-
16-
const canvas = canvasRef.current;
17-
const context = canvas.getContext('2d');
18-
19-
// Clear canvas and set background
20-
context.fillStyle = style.backgroundColor;
21-
context.fillRect(0, 0, width, height);
2215

23-
// Draw frequency bars
24-
if (freqs && freqs.length > 0) {
25-
context.strokeStyle = style.barColor;
26-
context.lineWidth = 1;
27-
context.beginPath();
28-
const logXRange = [Math.log(xRange[0]), Math.log(xRange[1])];
29-
const logRange = logXRange[1] - logXRange[0];
30-
31-
freqs.forEach(([freq, amp]) => {
32-
const canvasX = freq <= 0 ? 0 : ((Math.log(freq) - logXRange[0]) / logRange) * width;
33-
const barHeight = amp * height;
34-
if (canvasX >= 0 && canvasX <= width) {
35-
context.moveTo(canvasX, height);
36-
context.lineTo(canvasX, height - barHeight);
37-
}
38-
});
39-
context.stroke();
16+
function drawAxes(ctx, width, height, isLogarithmic, xRange) {
17+
const paddingLeft = 45;
18+
const paddingBottom = 45; //
19+
const paddingTop = 10;
20+
21+
ctx.save();
22+
ctx.strokeStyle = style.axisColor;
23+
ctx.fillStyle = style.textColor;
24+
ctx.lineWidth = 1;
25+
ctx.font = '12px sans-serif';
26+
ctx.textAlign = 'center';
27+
ctx.textBaseline = 'middle';
28+
29+
// ---- Axis lines ----
30+
ctx.beginPath();
31+
ctx.moveTo(paddingLeft, height - paddingBottom);
32+
ctx.lineTo(width - 10, height - paddingBottom);
33+
ctx.stroke();
34+
35+
ctx.beginPath();
36+
ctx.moveTo(paddingLeft, paddingTop);
37+
ctx.lineTo(paddingLeft, height - paddingBottom);
38+
ctx.stroke();
39+
40+
// ---- Axis Labels ----
41+
ctx.fillText('Frequency (Hz)', width / 2, height - 8);
42+
43+
ctx.save();
44+
ctx.translate(15, height / 2);
45+
ctx.rotate(-Math.PI / 2);
46+
ctx.fillText('Amplitude', 0, 0);
47+
ctx.restore();
48+
49+
// ---- Frequency (Hz) ----
50+
let xTicks;
51+
if (isLogarithmic) {
52+
const [minFreq, maxFreq] = xRange;
53+
const logMin = Math.log10(minFreq);
54+
const logMax = Math.log10(maxFreq);
55+
xTicks = [];
56+
for (let p = Math.ceil(logMin); p <= Math.floor(logMax); p++) {
57+
const base = Math.pow(10, p);
58+
xTicks.push(base);
59+
if (base * 2 <= maxFreq) xTicks.push(base * 2);
60+
if (base * 5 <= maxFreq) xTicks.push(base * 5);
4061
}
41-
}, [freqs, width, height, xRange]); // Effect for frequency bars
62+
xTicks = xTicks.filter(v => v >= minFreq && v <= maxFreq).sort((a, b) => a - b);
63+
} else {
64+
const [min, max] = xRange;
65+
const step = (max - min) / 5;
66+
xTicks = Array.from({ length: 6 }, (_, i) => min + i * step);
67+
}
68+
69+
// ---- Draw X-axis ticks ----
70+
xTicks.forEach(tick => {
71+
const x = isLogarithmic
72+
? ((Math.log(tick) - Math.log(xRange[0])) /
73+
(Math.log(xRange[1]) - Math.log(xRange[0]))) *
74+
(width - paddingLeft - 20) + paddingLeft
75+
: ((tick - xRange[0]) / (xRange[1] - xRange[0])) *
76+
(width - paddingLeft - 20) + paddingLeft;
77+
78+
ctx.beginPath();
79+
ctx.moveTo(x, height - paddingBottom);
80+
ctx.lineTo(x, height - paddingBottom + 5);
81+
ctx.stroke();
82+
83+
const label = tick >= 1000 ? `${tick / 1000}k` : `${tick.toString()}`;
84+
ctx.fillText(label, x, height - paddingBottom + 15);
85+
});
86+
87+
// ---- Y-axis ticks ----
88+
const yTicks = [-12, -6, 0, 6, 12]; // This seems to be a placeholder, as nodes are 0-1.
89+
const toCanvasY = val => {
90+
const norm = (val + 12) / 24; // [-12..12] → [0..1]
91+
return height - paddingBottom - norm * (height - paddingBottom - paddingTop);
92+
};
93+
94+
ctx.textAlign = 'right';
95+
yTicks.forEach(val => {
96+
const y = toCanvasY(val);
97+
// Tick line
98+
ctx.strokeStyle = style.axisColor;
99+
ctx.beginPath();
100+
ctx.moveTo(paddingLeft - 5, y);
101+
ctx.lineTo(paddingLeft, y);
102+
ctx.stroke();
103+
// Label
104+
ctx.fillText(`${val}`, paddingLeft - 8, y);
105+
// Grid line
106+
ctx.strokeStyle = style.gridColor;
107+
ctx.beginPath();
108+
ctx.moveTo(paddingLeft, y);
109+
ctx.lineTo(width - 10, y);
110+
ctx.stroke();
111+
});
112+
113+
ctx.restore();
114+
}
42115

116+
const useCanvasDrawing = (canvasRef, { wasmModule, width, height, nodes, xRange, curves, freqs, isLogarithmic, onWheel }) => {
43117
const envelopePath = useMemo(() => {
44118
if (!wasmModule || nodes.length < 2 || !width || !height) {
45119
return null;
46120
}
47121

48-
const logXRange = [Math.log(xRange[0]), Math.log(xRange[1])];
49-
const logRange = logXRange[1] - logXRange[0];
50-
51-
const getCanvasPoint = (node) => {
52-
let canvasX;
53-
if (isLogarithmic) {
54-
canvasX = node.x <= 0 ? 0 : ((Math.log(node.x) - logXRange[0]) / (logXRange[1] - logXRange[0])) * width;
55-
} else {
56-
canvasX = ((node.x - xRange[0]) / (xRange[1] - xRange[0])) * width;
57-
}
122+
const getCanvasPoint = (node, logX, xRangeLinear) => {
123+
const canvasX = isLogarithmic
124+
? node.x <= 0 ? 0 : ((Math.log(node.x) - logX.min) / logX.range) * width
125+
: ((node.x - xRange[0]) / xRangeLinear) * width;
58126
const canvasY = (1 - node.y) * height;
59127
return { x: canvasX, y: canvasY };
60128
};
61129

130+
const logX = { min: Math.log(xRange[0]), max: Math.log(xRange[1]), range: Math.log(xRange[1]) - Math.log(xRange[0]) };
131+
const xRangeLinear = xRange[1] - xRange[0];
132+
62133
const path = new Path2D();
63-
const firstNodePoint = getCanvasPoint(nodes[0]);
134+
const firstNodePoint = getCanvasPoint(nodes[0], logX, xRangeLinear);
64135
path.moveTo(firstNodePoint.x, firstNodePoint.y);
65136

66137
for (let i = 0; i < nodes.length - 1; i++) {
67138
const startNode = nodes[i];
68139
const endNode = nodes[i + 1];
69140
const shape = curves[i] !== undefined ? curves[i] : 0;
70-
71141
const logStartX = Math.log(startNode.x);
72-
const logEndX = Math.log(endNode.x);
73-
const logXNodeRange = logEndX - logStartX;
142+
const logXNodeRange = Math.log(endNode.x) - logStartX;
74143

75-
const segments = 100;
144+
145+
const segments = 20;
76146
for (let j = 1; j <= segments; j++) {
77147
const t = j / segments;
78148
const logCurrentX = logStartX + t * logXNodeRange;
79-
const currentX = ((logCurrentX - logXRange[0]) / logRange) * width;
149+
const currentX = ((logCurrentX - logX.min) / logX.range) * width;
80150
const linearY = startNode.y + t * (endNode.y - startNode.y);
81151
const curveOffset = wasmModule.applyShape(t, shape);
82152
const currentY = (1 - (linearY - curveOffset * t * (1 - t))) * height;
@@ -86,40 +156,62 @@ const useCanvasDrawing = (canvasRef, { wasmModule, width, height, nodes, xRange,
86156
return path;
87157
}, [wasmModule, nodes, curves, width, height, xRange, isLogarithmic]);
88158

89-
useEffect(() => {
90-
if (!width || !height) return;
159+
const draw = useCallback((context) => {
160+
context.fillStyle = style.backgroundColor;
161+
context.fillRect(0, 0, width, height);
91162

92-
const canvas = canvasRef.current;
93-
const context = canvas.getContext('2d');
163+
drawAxes(context, width, height, isLogarithmic, xRange);
164+
165+
const logXRange = { min: Math.log(xRange[0]), max: Math.log(xRange[1]), range: Math.log(xRange[1]) - Math.log(xRange[0]) };
166+
167+
if (freqs && freqs.length > 0) {
168+
context.strokeStyle = style.barColor;
169+
context.lineWidth = 1;
170+
context.beginPath();
171+
172+
freqs.forEach(([freq, amp]) => {
173+
const canvasX = freq <= 0 ? 0 : ((Math.log(freq) - logXRange.min) / logXRange.range) * width;
174+
const barHeight = amp * height;
175+
if (canvasX >= 0 && canvasX <= width) {
176+
context.lineTo(canvasX, height - barHeight);
177+
}
178+
});
179+
context.stroke();
180+
}
94181

95-
// Draw the cached envelope path
96182
if (envelopePath) {
97183
context.strokeStyle = style.connectorColor;
98184
context.lineWidth = style.connectorWidth;
99185
context.stroke(envelopePath);
100186
}
101187

102-
// Draw nodes (these need to be redrawn as they move)
103188
context.fillStyle = style.nodeColor;
104-
const logXRange = [Math.log(xRange[0]), Math.log(xRange[1])];
105-
const logRange = logXRange[1] - logXRange[0];
106189
nodes.forEach(node => {
107-
const x = node.x <= 0 ? 0 : ((Math.log(node.x) - logXRange[0]) / logRange) * width;
190+
const x = node.x <= 0 ? 0 : ((Math.log(node.x) - logXRange.min) / logXRange.range) * width;
108191
const y = (1 - node.y) * height;
109192
context.beginPath();
110193
context.arc(x, y, style.nodeRadius, 0, 2 * Math.PI);
111194
context.fill();
112195
});
196+
}, [width, height, isLogarithmic, xRange, freqs, nodes, envelopePath]);
113197

114-
if (onWheel) {
115-
// The wheel event listener must be registered with { passive: false }
116-
// to allow calling e.preventDefault().
117-
canvas.addEventListener('wheel', onWheel, { passive: false });
118-
return () => {
119-
canvas.removeEventListener('wheel', onWheel);
120-
};
121-
}
122-
}, [canvasRef, width, height, nodes, xRange, onWheel, envelopePath]); // Effect for envelope and nodes
198+
useEffect(() => {
199+
if (!width || !height || !canvasRef.current) return;
200+
201+
const canvas = canvasRef.current;
202+
const context = canvas.getContext('2d');
203+
let animationFrameId;
204+
205+
const render = () => {
206+
draw(context);
207+
animationFrameId = window.requestAnimationFrame(render);
208+
};
209+
render();
210+
211+
return () => {
212+
window.cancelAnimationFrame(animationFrameId);
213+
};
214+
}, [draw, width, height, canvasRef]);
123215
};
124216

125217
export default useCanvasDrawing;

src/pages/Sonara.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { applyEnvelope } from '../utils/applyEnvelope';
88

99
function Sonara() {
1010
const displayWidth = 800;
11-
const displayHeight = 250;
11+
const displayHeight = 350;
1212

1313
const [adsr, setAdsr] = useState({ attack: 0.1, decay: 0.2, sustain: 0.7, release: 0.5 });
1414
const [wasmModule, setWasmModule] = useState(null);

0 commit comments

Comments
 (0)