Skip to content

Commit f63899e

Browse files
committed
feat: Add real-time operator level metering to the UI, driven by audio worklet output.
1 parent a1f79f4 commit f63899e

File tree

4 files changed

+50
-6
lines changed

4 files changed

+50
-6
lines changed

App.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,15 @@ const App: React.FC = () => {
3030
const engineRef = useRef<DX7Engine | null>(null);
3131
const fileInputRef = useRef<HTMLInputElement>(null);
3232
const lastSyncTimeRef = useRef<number>(0);
33+
const [opLevels, setOpLevels] = useState<Float32Array>(new Float32Array(6));
3334

3435
useEffect(() => {
3536
engineRef.current = new DX7Engine(PRESETS[0]);
37+
engineRef.current.onOpLevels((levels) => {
38+
// requestAnimationFrame to throttle UI updates if needed, but react set state is already batched usually.
39+
// However, 50ms updates are fine.
40+
setOpLevels(levels);
41+
});
3642
}, []);
3743

3844
useEffect(() => {
@@ -279,7 +285,7 @@ const App: React.FC = () => {
279285
</div>
280286
<div className="flex flex-col">
281287
{patch.operators.map((op, i) => (
282-
<OperatorPanel key={i + 1} index={i + 1} params={op} onChange={p => {
288+
<OperatorPanel key={i + 1} index={i + 1} params={op} level={opLevels[i]} onChange={p => {
283289
const next = [...patch.operators]; next[i] = p; updatePatch({ operators: next });
284290
}} />
285291
))}

components/OperatorPanel.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ interface OperatorPanelProps {
77
key?: React.Key;
88
index: number;
99
params: OperatorParams;
10+
level?: number;
1011
onChange: (newParams: OperatorParams) => void;
1112
}
1213

@@ -39,7 +40,7 @@ const CurveIcon: React.FC<{ type: number; mirrored?: boolean }> = ({ type, mirro
3940
);
4041
};
4142

42-
const OperatorPanel: React.FC<OperatorPanelProps> = ({ index, params, onChange }) => {
43+
const OperatorPanel: React.FC<OperatorPanelProps> = ({ index, params, level, onChange }) => {
4344
const update = (field: keyof OperatorParams, value: any) => {
4445
onChange({ ...params, [field]: value });
4546
};
@@ -86,13 +87,22 @@ const OperatorPanel: React.FC<OperatorPanelProps> = ({ index, params, onChange }
8687
<div className={`flex items-center border-b border-white/10 transition-all relative w-full ${isActive ? 'bg-[#1a1a1a]' : 'bg-[#0f0f0f] opacity-60'}`}>
8788

8889
{/* Sidebar Toggle Section */}
89-
<div className="flex flex-col items-center justify-center min-w-[80px] w-[80px] lg:min-w-[40px] lg:w-[40px] h-full border-r border-white/5 bg-black/40 shrink-0 z-10">
90+
<div className="flex flex-col items-center justify-center min-w-[80px] w-[80px] lg:min-w-[40px] lg:w-[40px] h-full border-r border-white/5 bg-black/40 shrink-0 z-10 relative overflow-hidden">
91+
9092
<button
9193
onClick={toggleOperator}
92-
className={`w-10 h-10 lg:w-7 lg:h-7 rounded-sm flex items-center justify-center border transition-all font-orbitron font-bold text-sm lg:text-[10px] active:scale-90 select-none ${isActive ? 'bg-black text-dx7-teal border-dx7-teal shadow-[0_0_15px_rgba(0,212,193,0.3)]' : 'bg-[#080808] text-gray-700 border-[#1a1a1a]'}`}
94+
className={`relative z-10 w-10 h-10 lg:w-7 lg:h-7 rounded-sm flex items-center justify-center border transition-all font-orbitron font-bold text-sm lg:text-[10px] active:scale-90 select-none ${isActive ? 'bg-black text-dx7-teal border-dx7-teal shadow-[0_0_15px_rgba(0,212,193,0.3)]' : 'bg-[#080808] text-gray-700 border-[#1a1a1a]'}`}
9395
>
9496
{index}
9597
</button>
98+
99+
{/* High Visibility Level Meter (Right Edge) */}
100+
<div className="absolute right-0 top-0 bottom-0 w-[3px] bg-black/50">
101+
<div
102+
className="absolute bottom-0 left-0 right-0 bg-dx7-teal shadow-[0_0_8px_#00d4c1] transition-all duration-75 ease-out"
103+
style={{ height: `${Math.min(100, (level || 0) * 150)}%` }} // Increased sensitivity (1.5x)
104+
/>
105+
</div>
96106
</div>
97107

98108
{/* Control Surface */}

public/dx7-processor.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,7 @@ class DX7Processor extends AudioWorkletProcessor {
440440
this.sustain = false;
441441
this.lfo = null;
442442
this.algorithms = null; // Received via message
443+
this.counter = 0; // For metering timing
443444

444445
this.port.onmessage = e => {
445446
const { type, data, algorithms } = e.data;
@@ -500,6 +501,21 @@ class DX7Processor extends AudioWorkletProcessor {
500501
}
501502
outL[i] = l; outR[i] = r;
502503
}
504+
505+
// Metering: Send operator levels every ~46ms (2048 samples)
506+
if (this.counter % 2048 === 0) {
507+
const levels = new Float32Array(6);
508+
// Find max level for each operator across all active voices
509+
for (const v of this.voices) {
510+
for (let op = 0; op < 6; op++) {
511+
const val = Math.abs(v.opOutputs[op]);
512+
if (val > levels[op]) levels[op] = val;
513+
}
514+
}
515+
this.port.postMessage({ type: 'opLevels', data: levels });
516+
}
517+
this.counter += outL.length;
518+
503519
return true;
504520
}
505521
}

services/engine.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,21 @@ export class DX7Engine {
1919
alert(msg); throw new Error(msg);
2020
}
2121

22-
// Use relative path for better compatibility
23-
await this.context.audioWorklet.addModule('dx7-processor.js');
22+
// Use relative path with cache busting
23+
await this.context.audioWorklet.addModule(`dx7-processor.js?v=${Date.now()}`);
2424

2525
this.node = new AudioWorkletNode(this.context, 'dx7-processor', {
2626
outputChannelCount: [2],
2727
numberOfInputs: 0,
2828
numberOfOutputs: 1
2929
});
3030

31+
this.node.port.onmessage = (e) => {
32+
if (e.data.type === 'opLevels' && this.opLevelsHandler) {
33+
this.opLevelsHandler(e.data.data);
34+
}
35+
};
36+
3137
this.node.connect(this.context.destination);
3238

3339
// Initialize algorithms data in the worklet
@@ -49,8 +55,14 @@ export class DX7Engine {
4955
noteOff(note: number) { this.node?.port.postMessage({ type: 'noteOff', data: { note } }); }
5056
panic() { this.node?.port.postMessage({ type: 'panic' }); }
5157

58+
private opLevelsHandler: ((levels: Float32Array) => void) | null = null;
59+
5260
setPitchBend(val: number) { this.node?.port.postMessage({ type: 'pitchBend', data: val }); }
5361
setModWheel(val: number) { this.node?.port.postMessage({ type: 'modWheel', data: val }); }
5462
setAftertouch(val: number) { this.node?.port.postMessage({ type: 'aftertouch', data: val }); }
5563
setSustain(val: boolean) { this.node?.port.postMessage({ type: 'sustain', data: val }); }
64+
65+
public onOpLevels(callback: (levels: Float32Array) => void) {
66+
this.opLevelsHandler = callback;
67+
}
5668
}

0 commit comments

Comments
 (0)