Skip to content

Commit 6060bb3

Browse files
committed
Added a Peak Meter
1 parent 2d50808 commit 6060bb3

File tree

5 files changed

+110
-17
lines changed

5 files changed

+110
-17
lines changed

src/App.ts

Lines changed: 89 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,89 @@ import { InflictorFactory } from "./audio/plugins/Inflictor";
2323
import { FilterFactory } from "./audio/plugins/Filter";
2424
import { ChorusFactory } from "./audio/plugins/Chorus";
2525

26+
class PeakMeter implements IComponent {
27+
app: Appl;
28+
container: HTMLDivElement;
29+
canvas: HTMLCanvasElement;
30+
analyserNode: AnalyserNode;
31+
dataArray: Uint8Array;
32+
intervalId: number | null = null;
33+
34+
constructor(app: Appl) {
35+
this.app = app;
36+
37+
this.container = document.createElement("div");
38+
this.container.classList.add("flex", "items-center", "w-48", "bg-neutral-900", "rounded");
39+
40+
this.canvas = document.createElement("canvas");
41+
this.canvas.width = 192;
42+
this.canvas.height = 32;
43+
this.canvas.style.display = "block";
44+
this.container.appendChild(this.canvas);
45+
}
46+
47+
onUpdate = () => {
48+
this.analyserNode.getByteTimeDomainData(this.dataArray as Uint8Array<ArrayBuffer>); // WTF
49+
50+
let max = 0;
51+
for (let i = 0; i < this.dataArray.length; i++) {
52+
max = Math.max(Math.abs(this.dataArray[i] - 128), max);
53+
}
54+
55+
const level = Math.log10(1 + max) / Math.log10(128) * 100;
56+
57+
const width = this.canvas.width;
58+
const height = this.canvas.height;
59+
const fillWidth = (level / 100) * width;
60+
61+
const context = this.canvas.getContext("2d");
62+
context.clearRect(0, 0, width, height);
63+
64+
const greenEnd = 0.7 * width;
65+
if (fillWidth > 0) {
66+
context.fillStyle = "green";
67+
context.fillRect(0, 0, Math.min(fillWidth, greenEnd), height);
68+
}
69+
70+
const yellowEnd = 0.8 * width;
71+
if (fillWidth > greenEnd) {
72+
context.fillStyle = "yellow";
73+
context.fillRect(greenEnd, 0, Math.min(fillWidth, yellowEnd) - greenEnd, height);
74+
}
75+
76+
if (fillWidth > yellowEnd) {
77+
context.fillStyle = "red";
78+
context.fillRect(yellowEnd, 0, fillWidth - yellowEnd, height);
79+
}
80+
}
81+
82+
setDevice() {
83+
this.stopPolling();
84+
85+
this.analyserNode = new AnalyserNode(this.app.device.context, {
86+
fftSize: 1024,
87+
});
88+
89+
this.app.device.masterGainNode.connect(this.analyserNode);
90+
91+
const bufferLength = this.analyserNode.frequencyBinCount;
92+
this.dataArray = new Uint8Array(bufferLength);
93+
94+
this.intervalId = window.setInterval(this.onUpdate, 25);
95+
}
96+
97+
stopPolling() {
98+
if (this.intervalId !== null) {
99+
clearInterval(this.intervalId);
100+
this.intervalId = null;
101+
}
102+
}
103+
104+
getDomNode(): Node {
105+
return this.container;
106+
}
107+
}
108+
26109
class BpmInput implements IComponent {
27110

28111
app: Appl;
@@ -65,6 +148,7 @@ export class Appl extends CommandHost implements IComponent {
65148
fullscreen: FullScreen;
66149
menuBar: MenuBar;
67150
bpmInput: BpmInput;
151+
peakMeter: PeakMeter;
68152
toolbar: HTMLElement;
69153
frame: GridFrameContainer;
70154
mainTabs: TabFrameContainer;
@@ -137,8 +221,9 @@ export class Appl extends CommandHost implements IComponent {
137221
]);
138222

139223
this.bpmInput = new BpmInput(this);
224+
this.peakMeter = new PeakMeter(this);
140225

141-
const toolbarContainer = HFlex([toolbar, this.bpmInput.getDomNode()], "gap-1");
226+
const toolbarContainer = HFlex([toolbar, this.bpmInput.getDomNode(), this.peakMeter.getDomNode()], "gap-1");
142227

143228
this.mainTabs = new TabFrameContainer(false);
144229

@@ -226,9 +311,11 @@ export class Appl extends CommandHost implements IComponent {
226311
async setAudioDevice(outputDeviceId: string, inputDeviceId: string, latencySec: number) {
227312
await this.device.create(outputDeviceId, inputDeviceId, latencySec);
228313

229-
this.player = new Player(this.instrumentFactories, this.device.context);
314+
this.player = new Player(this.instrumentFactories, this.device);
230315
this.playerSongAdapter.attachPlayer(this.player);
231316

317+
this.peakMeter.setDevice();
318+
232319
await this.writeSetting("OutputDevice", outputDeviceId);
233320
await this.writeSetting("InputDevice", inputDeviceId);
234321
await this.writeSetting("Latency", latencySec);

src/audio/AudioDevice.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export class AudioDevice {
1212
context: AudioContext | null = null;
1313
inputNode: MediaStreamAudioSourceNode;
1414
recorder: Recorder;
15+
masterGainNode: GainNode
1516
inputMode: "stereo" | "left" | "right";
1617

1718
constructor() {
@@ -48,6 +49,9 @@ export class AudioDevice {
4849

4950
this.recorder = new Recorder(this.context);
5051
this.inputNode.connect(this.recorder.recordNode);
52+
53+
this.masterGainNode = new GainNode(this.context, { gain: 1.0 });
54+
this.masterGainNode.connect(this.context.destination);
5155
}
5256

5357
async close() {

src/audio/Player.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { AudioDevice } from "./AudioDevice";
12
import { Instrument, InstrumentFactory } from "./plugins/InstrumentFactory";
23
import { PatternColumnType } from "./SongDocument";
34

@@ -110,7 +111,7 @@ class PatternPlayer {
110111
}
111112

112113
export class Player extends EventTarget {
113-
context: AudioContext;
114+
device: AudioDevice;
114115
playing: boolean = false;
115116
startTime: number;
116117
currentTime: number;
@@ -126,10 +127,10 @@ export class Player extends EventTarget {
126127
loopEnd: number = 8;
127128
playingPatterns: PatternPlayer[] = [];
128129

129-
constructor(instrumentFactories: InstrumentFactory[], context: AudioContext) {
130+
constructor(instrumentFactories: InstrumentFactory[], device: AudioDevice) {
130131
super();
131132
this.instrumentFactories = instrumentFactories;
132-
this.context = context;
133+
this.device = device;
133134
}
134135

135136
getInstrumentFactoryById(id: string) {
@@ -153,14 +154,14 @@ export class Player extends EventTarget {
153154

154155
this.currentTime = 0;
155156
this.currentBeat = startBeat;
156-
this.startTime = this.context.currentTime;
157+
this.startTime = this.device.context.currentTime;
157158

158159
// schedule 1 second first, then reschedule after 500ms to fill so we remain 1 second ahead
159160
this.scheduleSequence(SCHEDULE_INTERVAL);
160161

161162
this.playInterval = setInterval(() => {
162163

163-
const until = this.context.currentTime + SCHEDULE_INTERVAL;
164+
const until = this.device.context.currentTime + SCHEDULE_INTERVAL;
164165
const duration = until - (this.startTime + this.currentTime);
165166

166167
// console.log("Player time", this.currentTime, "Duration", duration, "Dvic time", this.context.currentTime, "relative to", this.startTime, " compute beat", this.currentBeat);

src/audio/PlayerSongAdapter.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ export class PlayerSongAdapter {
182182
return;
183183
}
184184

185-
const instrument = factory.createInstrument(this.player.context, this.player);
185+
const instrument = factory.createInstrument(this.player.device.context, this.player);
186186

187187
this.player.instruments.push(instrument);
188188
this.instrumentMap.set(i, instrument);
@@ -221,7 +221,7 @@ export class PlayerSongAdapter {
221221
const connection = new Connection();
222222
connection.from = this.instrumentMap.get(c.from);
223223
connection.to = this.instrumentMap.get(c.to);
224-
connection.gainNode = this.player.context.createGain();
224+
connection.gainNode = this.player.device.context.createGain();
225225
connection.gainNode.gain.setValueAtTime(c.gain, 0);
226226

227227
connection.from.connect(connection.gainNode);
@@ -233,7 +233,7 @@ export class PlayerSongAdapter {
233233

234234
onUpdateConnection = (ev: CustomEvent<ConnectionDocument>) => {
235235
const connection = this.connectionMap.get(ev.detail);
236-
connection.gainNode.gain.linearRampToValueAtTime(ev.detail.gain, this.player.context.currentTime + 0.050);
236+
connection.gainNode.gain.linearRampToValueAtTime(ev.detail.gain, this.player.device.context.currentTime + 0.050);
237237
};
238238

239239
onDeleteConnection = (ev: CustomEvent<ConnectionDocument>) => {
@@ -255,7 +255,7 @@ export class PlayerSongAdapter {
255255

256256
const instrument = this.instrumentMap.get(w.instrument);
257257

258-
const audioBuffer = this.player.context.createBuffer(w.buffers.length, w.sampleCount, w.sampleRate);
258+
const audioBuffer = this.player.device.context.createBuffer(w.buffers.length, w.sampleCount, w.sampleRate);
259259
for (let i = 0; i < w.buffers.length; i++) {
260260
const buffer = audioBuffer.getChannelData(i);
261261
buffer.set(w.buffers[i]);
@@ -281,7 +281,7 @@ export class PlayerSongAdapter {
281281
wave.name = w.name;
282282

283283
if (w.sampleCount !== wave.sampleCount) {
284-
const audioBuffer = this.player.context.createBuffer(w.buffers.length, w.sampleCount, w.sampleRate);
284+
const audioBuffer = this.player.device.context.createBuffer(w.buffers.length, w.sampleCount, w.sampleRate);
285285
for (let i = 0; i < w.buffers.length; i++) {
286286
const buffer = audioBuffer.getChannelData(i);
287287
buffer.set(w.buffers[i]);

src/audio/plugins/Master.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,23 @@
1+
import { Player } from "../Player";
12
import { Instrument, InstrumentFactory } from "./InstrumentFactory";
23

34
export class MasterFactory extends InstrumentFactory {
45
get identifier(): string {
56
return "@modulyzer/Master";
67
}
78

8-
createInstrument(context: AudioContext): Instrument {
9-
return new Master(context, this);
9+
createInstrument(context: AudioContext, player: Player): Instrument {
10+
return new Master(this, player);
1011
}
1112
}
1213

1314
export class Master extends Instrument {
14-
context: AudioContext;
15+
player: Player;
1516

16-
constructor(context: AudioContext, factory: InstrumentFactory) {
17+
constructor(factory: InstrumentFactory, player: Player) {
1718
super(factory);
1819
this.outputNode = null;
19-
this.inputNode = context.destination;
20+
this.inputNode = player.device.masterGainNode;
2021
this.parameters = [];
2122
}
2223

0 commit comments

Comments
 (0)