awacke1 commited on
Commit
a92c685
·
verified ·
1 Parent(s): b0f4bf5

Create app.js

Browse files
Files changed (1) hide show
  1. javascript/app.js +372 -0
javascript/app.js ADDED
@@ -0,0 +1,372 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const MIDI_OUTPUT_BATCH_SIZE = 4; // Automatically set by Python backend, do not change.
2
+
3
+ // Utility to query elements across shadow DOM
4
+ function deepQuerySelector(selector) {
5
+ function deepSearch(root, selector) {
6
+ let element = root.querySelector(selector);
7
+ if (element) return element;
8
+ const shadowHosts = root.querySelectorAll('*');
9
+ for (let host of shadowHosts) {
10
+ if (host.shadowRoot) {
11
+ element = deepSearch(host.shadowRoot, selector);
12
+ if (element) return element;
13
+ }
14
+ }
15
+ return null;
16
+ }
17
+ return deepSearch(this, selector);
18
+ }
19
+ Element.prototype.deepQuerySelector = deepQuerySelector;
20
+ Document.prototype.deepQuerySelector = deepQuerySelector;
21
+
22
+ // Get Gradio app root
23
+ function gradioApp() {
24
+ const elems = document.getElementsByTagName('gradio-app');
25
+ const gradioShadowRoot = elems.length ? elems[0].shadowRoot : null;
26
+ return gradioShadowRoot || document;
27
+ }
28
+
29
+ // Callback registries
30
+ const uiUpdateCallbacks = [];
31
+ const msgReceiveCallbacks = [];
32
+
33
+ function onUiUpdate(callback) {
34
+ uiUpdateCallbacks.push(callback);
35
+ }
36
+
37
+ function onMsgReceive(callback) {
38
+ msgReceiveCallbacks.push(callback);
39
+ }
40
+
41
+ function runCallback(callback, data) {
42
+ try {
43
+ callback(data);
44
+ } catch (e) {
45
+ console.error(e);
46
+ }
47
+ }
48
+
49
+ function executeCallbacks(queue, data) {
50
+ queue.forEach((callback) => runCallback(callback, data));
51
+ }
52
+
53
+ // Observe DOM changes
54
+ document.addEventListener("DOMContentLoaded", () => {
55
+ const observer = new MutationObserver((mutations) => {
56
+ executeCallbacks(uiUpdateCallbacks, mutations);
57
+ });
58
+ observer.observe(gradioApp(), { childList: true, subtree: true });
59
+ });
60
+
61
+ // HSV to RGB conversion for note coloring
62
+ function HSVtoRGB(h, s, v) {
63
+ let r, g, b, i = Math.floor(h * 6), f = h * 6 - i, p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s);
64
+ switch (i % 6) {
65
+ case 0: r = v; g = t; b = p; break;
66
+ case 1: r = q; g = v; b = p; break;
67
+ case 2: r = p; g = v; b = t; break;
68
+ case 3: r = p; g = q; b = v; break;
69
+ case 4: r = t; g = p; b = v; break;
70
+ case 5: r = v; g = p; b = q; break;
71
+ }
72
+ return { r: Math.round(r * 255), g: Math.round(g * 255), b: Math.round(b * 255) };
73
+ }
74
+
75
+ // Detect mobile devices
76
+ function isMobile() {
77
+ return /iPhone|iPad|iPod|Android|Windows Phone/i.test(navigator.userAgent);
78
+ }
79
+
80
+ // MIDI Visualizer Custom Element
81
+ class MidiVisualizer extends HTMLElement {
82
+ constructor() {
83
+ super();
84
+ this.midiEvents = [];
85
+ this.activeNotes = [];
86
+ this.midiTimes = [];
87
+ this.trackMap = new Map();
88
+ this.patches = Array(16).fill().map(() => [[0, 0]]);
89
+ this.config = { noteHeight: isMobile() ? 1 : 4, beatWidth: isMobile() ? 16 : 32 };
90
+ this.timePreBeat = 16;
91
+ this.svgWidth = 0;
92
+ this.t1 = 0;
93
+ this.totalTimeMs = 0;
94
+ this.playTime = 0;
95
+ this.playTimeMs = 0;
96
+ this.lastUpdateTime = 0;
97
+ this.colorMap = new Map();
98
+ this.playing = false;
99
+ this.version = "v2";
100
+ this.init();
101
+ }
102
+
103
+ init() {
104
+ this.innerHTML = '';
105
+ const shadow = this.attachShadow({ mode: 'open' });
106
+ const style = document.createElement("style");
107
+ style.textContent = ".note.active {stroke: black; stroke-width: 0.75; stroke-opacity: 0.75;}";
108
+ const container = document.createElement('div');
109
+ container.style.display = "flex";
110
+ container.style.height = `${this.config.noteHeight * 128 + 25}px`;
111
+ const pianoRoll = document.createElement('div');
112
+ pianoRoll.style.overflowX = "scroll";
113
+ pianoRoll.style.flexGrow = "1";
114
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
115
+ svg.style.height = `${this.config.noteHeight * 128}px`;
116
+ const timeLine = document.createElementNS('http://www.w3.org/2000/svg', 'line');
117
+ timeLine.style.stroke = "green";
118
+ timeLine.style.strokeWidth = isMobile() ? "1" : "2";
119
+
120
+ shadow.appendChild(style);
121
+ shadow.appendChild(container);
122
+ container.appendChild(pianoRoll);
123
+ pianoRoll.appendChild(svg);
124
+ svg.appendChild(timeLine);
125
+
126
+ this.container = container;
127
+ this.pianoRoll = pianoRoll;
128
+ this.svg = svg;
129
+ this.timeLine = timeLine;
130
+ for (let i = 0; i < 128; i++) {
131
+ this.colorMap.set(i, HSVtoRGB(i / 128, 1, 1));
132
+ }
133
+ this.setPlayTime(0);
134
+ }
135
+
136
+ getTrack(tr, cl) {
137
+ const id = tr * 16 + cl;
138
+ let track = this.trackMap.get(id);
139
+ if (!track) {
140
+ const color = this.colorMap.get((this.trackMap.size * 53) % 128);
141
+ track = { id, tr, cl, svg: document.createElementNS('http://www.w3.org/2000/svg', 'g'), color };
142
+ this.svg.appendChild(track.svg);
143
+ this.trackMap.set(id, track);
144
+ }
145
+ return track;
146
+ }
147
+
148
+ clearMidiEvents() {
149
+ this.midiEvents = [];
150
+ this.activeNotes = [];
151
+ this.midiTimes = [];
152
+ this.trackMap.clear();
153
+ this.patches = Array(16).fill().map(() => [[0, 0]]);
154
+ this.t1 = 0;
155
+ this.setPlayTime(0);
156
+ this.totalTimeMs = 0;
157
+ this.playTimeMs = 0;
158
+ this.lastUpdateTime = 0;
159
+ this.svgWidth = 0;
160
+ this.svg.innerHTML = '';
161
+ this.svg.appendChild(this.timeLine);
162
+ this.svg.style.width = `${this.svgWidth}px`;
163
+ }
164
+
165
+ appendMidiEvent(midiEvent) {
166
+ if (Array.isArray(midiEvent) && midiEvent.length > 0) {
167
+ this.t1 += midiEvent[1];
168
+ const t = this.t1 * this.timePreBeat + midiEvent[2];
169
+ midiEvent = [midiEvent[0], t].concat(midiEvent.slice(3));
170
+ if (midiEvent[0] === "note") {
171
+ const track = midiEvent[2];
172
+ const channel = midiEvent[3];
173
+ const pitch = midiEvent[4];
174
+ const velocity = midiEvent[5];
175
+ const duration = midiEvent[6];
176
+ const visTrack = this.getTrack(track, channel);
177
+ const x = (t / this.timePreBeat) * this.config.beatWidth;
178
+ const y = (127 - pitch) * this.config.noteHeight;
179
+ const w = (duration / this.timePreBeat) * this.config.beatWidth;
180
+ const h = this.config.noteHeight;
181
+ this.svgWidth = Math.ceil(Math.max(x + w, this.svgWidth));
182
+ const opacity = Math.min(1, velocity / 127 + 0.1).toFixed(2);
183
+ const rect = this.drawNote(visTrack, x, y, w, h, opacity);
184
+ midiEvent.push(rect);
185
+ this.setPlayTime(t);
186
+ this.pianoRoll.scrollTo(this.svgWidth - this.pianoRoll.offsetWidth, 0);
187
+ }
188
+ this.midiEvents.push(midiEvent);
189
+ this.svg.style.width = `${this.svgWidth}px`;
190
+ }
191
+ }
192
+
193
+ drawNote(track, x, y, w, h, opacity) {
194
+ const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
195
+ rect.classList.add('note');
196
+ const color = track.color;
197
+ rect.setAttribute('fill', `rgba(${color.r}, ${color.g}, ${color.b}, ${opacity})`);
198
+ rect.setAttribute('x', `${Math.round(x)}`);
199
+ rect.setAttribute('y', `${Math.round(y)}`);
200
+ rect.setAttribute('width', `${Math.round(w)}`);
201
+ rect.setAttribute('height', `${Math.round(h)}`);
202
+ track.svg.appendChild(rect);
203
+ return rect;
204
+ }
205
+
206
+ finishAppendMidiEvent() {
207
+ this.midiEvents.sort((a, b) => a[1] - b[1]);
208
+ let tempo = (60 / 120) * 1000;
209
+ let ms = 0;
210
+ let lastT = 0;
211
+ this.midiTimes.push({ ms, t: 0, tempo });
212
+ this.midiEvents.forEach((midiEvent) => {
213
+ const t = midiEvent[1];
214
+ ms += ((t - lastT) / this.timePreBeat) * tempo;
215
+ if (midiEvent[0] === "note") {
216
+ this.totalTimeMs = Math.max(this.totalTimeMs, ms + (midiEvent[6] / this.timePreBeat) * tempo);
217
+ } else {
218
+ this.totalTimeMs = Math.max(this.totalTimeMs, ms);
219
+ }
220
+ lastT = t;
221
+ });
222
+ }
223
+
224
+ setPlayTime(t) {
225
+ this.playTime = t;
226
+ const x = Math.round((t / this.timePreBeat) * this.config.beatWidth);
227
+ this.timeLine.setAttribute('x1', `${x}`);
228
+ this.timeLine.setAttribute('y1', '0');
229
+ this.timeLine.setAttribute('x2', `${x}`);
230
+ this.timeLine.setAttribute('y2', `${this.config.noteHeight * 128}`);
231
+ this.pianoRoll.scrollTo(Math.max(0, x - this.pianoRoll.offsetWidth / 2), 0);
232
+
233
+ if (this.playing && Date.now() - this.lastUpdateTime > 50) {
234
+ const activeNotes = [];
235
+ this.removeActiveNotes(this.activeNotes);
236
+ this.midiEvents.forEach((midiEvent) => {
237
+ if (midiEvent[0] === "note") {
238
+ const time = midiEvent[1];
239
+ const duration = midiEvent[6];
240
+ const note = midiEvent[midiEvent.length - 1];
241
+ if (time <= this.playTime && time + duration >= this.playTime) {
242
+ activeNotes.push(note);
243
+ }
244
+ }
245
+ });
246
+ this.addActiveNotes(activeNotes);
247
+ this.lastUpdateTime = Date.now();
248
+ }
249
+ }
250
+
251
+ addActiveNotes(notes) {
252
+ notes.forEach((note) => {
253
+ this.activeNotes.push(note);
254
+ note.classList.add('active');
255
+ });
256
+ }
257
+
258
+ removeActiveNotes(notes) {
259
+ notes.forEach((note) => {
260
+ const idx = this.activeNotes.indexOf(note);
261
+ if (idx > -1) this.activeNotes.splice(idx, 1);
262
+ note.classList.remove('active');
263
+ });
264
+ }
265
+
266
+ play() {
267
+ this.playing = true;
268
+ }
269
+
270
+ pause() {
271
+ this.removeActiveNotes(this.activeNotes);
272
+ this.playing = false;
273
+ }
274
+
275
+ bindAudioPlayer(audio) {
276
+ this.pause();
277
+ audio.addEventListener("play", () => this.play());
278
+ audio.addEventListener("pause", () => this.pause());
279
+ audio.addEventListener("loadedmetadata", () => {
280
+ this.totalTimeMs = audio.duration * 1000;
281
+ });
282
+ audio.addEventListener("timeupdate", () => {
283
+ this.setPlayTimeMs(audio.currentTime * 1000);
284
+ });
285
+ }
286
+ }
287
+
288
+ customElements.define('midi-visualizer', MidiVisualizer);
289
+
290
+ // Setup visualizers and progress bar
291
+ (() => {
292
+ const midiVisualizers = Array.from({ length: MIDI_OUTPUT_BATCH_SIZE }, () => document.createElement('midi-visualizer'));
293
+ midiVisualizers.forEach((visualizer, idx) => {
294
+ onUiUpdate(() => {
295
+ const app = gradioApp();
296
+ const container = app.querySelector(`#midi_visualizer_container_${idx}`);
297
+ if (container && !container.contains(visualizer)) {
298
+ container.appendChild(visualizer);
299
+ }
300
+ const audio = app.querySelector(`#midi_audio_${idx} audio`);
301
+ if (audio) {
302
+ visualizer.bindAudioPlayer(audio);
303
+ }
304
+ });
305
+ });
306
+
307
+ let hasProgressBar = false;
308
+ let outputTabsInited = null;
309
+ onUiUpdate(() => {
310
+ const app = gradioApp();
311
+ const outputTabs = app.querySelector("#output_tabs");
312
+ if (outputTabs && outputTabsInited !== outputTabs) {
313
+ outputTabsInited = outputTabs;
314
+ }
315
+ });
316
+
317
+ function createProgressBar() {
318
+ const parent = outputTabsInited.parentNode;
319
+ const divProgress = document.createElement('div');
320
+ divProgress.className = 'progressDiv';
321
+ divProgress.style.width = "100%";
322
+ divProgress.style.background = "#b4c0cc";
323
+ divProgress.style.borderRadius = "8px";
324
+ const divInner = document.createElement('div');
325
+ divInner.className = 'progress';
326
+ divInner.style.color = "white";
327
+ divInner.style.background = "#0060df";
328
+ divInner.style.textAlign = "right";
329
+ divInner.style.fontWeight = "bold";
330
+ divInner.style.borderRadius = "8px";
331
+ divInner.style.height = "20px";
332
+ divInner.style.lineHeight = "20px";
333
+ divInner.style.paddingRight = "8px";
334
+ divInner.style.width = "0%";
335
+ divProgress.appendChild(divInner);
336
+ parent.insertBefore(divProgress, outputTabsInited);
337
+ hasProgressBar = true;
338
+ return divInner;
339
+ }
340
+
341
+ function setProgressBar(progress, total) {
342
+ if (!hasProgressBar) createProgressBar();
343
+ const divInner = gradioApp().querySelector(".progress");
344
+ if (divInner) {
345
+ divInner.style.width = `${(progress / total) * 100}%`;
346
+ divInner.textContent = `${progress}/${total}`;
347
+ }
348
+ }
349
+
350
+ onMsgReceive((msgs) => {
351
+ msgs.forEach((msg) => {
352
+ const { name, data } = msg;
353
+ const idx = data[0];
354
+ const visualizer = midiVisualizers[idx];
355
+ switch (name) {
356
+ case "visualizer_clear":
357
+ visualizer.clearMidiEvents();
358
+ break;
359
+ case "visualizer_append":
360
+ data[1].forEach((event) => visualizer.appendMidiEvent(event));
361
+ break;
362
+ case "visualizer_end":
363
+ visualizer.finishAppendMidiEvent();
364
+ visualizer.setPlayTime(0);
365
+ break;
366
+ case "progress":
367
+ setProgressBar(data[0], data[1]);
368
+ break;
369
+ }
370
+ });
371
+ });
372
+ })();