function gradioApp() { const elems = document.getElementsByTagName('gradio-app'); const gradioShadowRoot = elems.length === 0 ? null : elems[0].shadowRoot; return !!gradioShadowRoot ? gradioShadowRoot : document; } const uiUpdateCallbacks = []; const msgReceiveCallbacks = []; function onUiUpdate(callback) { uiUpdateCallbacks.push(callback); } function onMsgReceive(callback) { msgReceiveCallbacks.push(callback); } function runCallback(x, m) { try { x(m); } catch (e) { (console.error || console.log).call(console, e.message, e); } } function executeCallbacks(queue, m) { queue.forEach(function (x) { runCallback(x, m); }); } document.addEventListener('DOMContentLoaded', function () { const mutationObserver = new MutationObserver(function (m) { executeCallbacks(uiUpdateCallbacks, m); }); mutationObserver.observe(gradioApp(), { childList: true, subtree: true }); }); (function () { let mse_receiver_inited = null; onUiUpdate(() => { const app = gradioApp(); const msg_receiver = app.querySelector('#msg_receiver'); if (!!msg_receiver && mse_receiver_inited !== msg_receiver) { const mutationObserver = new MutationObserver(function (ms) { ms.forEach((m) => { m.addedNodes.forEach((node) => { if (node.nodeName === 'P') { const obj = JSON.parse(node.innerText); if (obj instanceof Array) { obj.forEach((o) => { executeCallbacks(msgReceiveCallbacks, o); }); } else { executeCallbacks(msgReceiveCallbacks, obj); } } }); }); }); mutationObserver.observe(msg_receiver, { childList: true, subtree: true, characterData: true, }); console.log('receiver init'); mse_receiver_inited = msg_receiver; } }); })(); function HSVtoRGB(h, s, v) { let r, g, b, i, f, p, q, t; i = Math.floor(h * 6); f = h * 6 - i; p = v * (1 - s); q = v * (1 - f * s); t = v * (1 - (1 - f) * s); switch (i % 6) { case 0: (r = v), (g = t), (b = p); break; case 1: (r = q), (g = v), (b = p); break; case 2: (r = p), (g = v), (b = t); break; case 3: (r = p), (g = q), (b = v); break; case 4: (r = t), (g = p), (b = v); break; case 5: (r = v), (g = p), (b = q); break; } return { r: Math.round(r * 255), g: Math.round(g * 255), b: Math.round(b * 255), }; } class MidiVisualizer extends HTMLElement { constructor() { super(); this.midiEvents = []; this.activeNotes = []; this.midiTimes = []; this.wrapper = null; this.svg = null; this.timeLine = null; this.config = { noteHeight: 4, beatWidth: 32, }; this.tickPreBeat = 500; this.svgWidth = 0; this.playTime = 0; this.playTimeMs = 0; this.colorMap = new Map(); this.playing = false; this.timer = null; this.init(); } init() { this.innerHTML = ''; const shadow = this.attachShadow({ mode: 'open' }); const style = document.createElement('style'); const wrapper = document.createElement('div'); style.textContent = '.note.active {stroke: black;stroke-width: 0.75;stroke-opacity: 0.75;}'; wrapper.style.overflowX = 'scroll'; const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); svg.style.height = `${this.config.noteHeight * 128}px`; svg.style.width = `${this.svgWidth}px`; const timeLine = document.createElementNS('http://www.w3.org/2000/svg', 'line'); timeLine.style.stroke = 'green'; timeLine.style.strokeWidth = 2; shadow.appendChild(style); shadow.appendChild(wrapper); wrapper.appendChild(svg); svg.appendChild(timeLine); this.wrapper = wrapper; this.svg = svg; this.timeLine = timeLine; this.setPlayTime(0); } clearMidiEvents() { this.pause(); this.midiEvents = []; this.activeNotes = []; this.midiTimes = []; this.colorMap.clear(); this.setPlayTime(0); this.playTimeMs = 0; this.svgWidth = 0; this.svg.innerHTML = ''; this.svg.style.width = `${this.svgWidth}px`; this.svg.appendChild(this.timeLine); } appendMidiEvent(midiEvent) { if (midiEvent instanceof Array && midiEvent.length > 0) { if (midiEvent[0] === 'note') { const t = midiEvent[1]; const duration = midiEvent[2]; const channel = midiEvent[3]; const pitch = midiEvent[4]; const velocity = midiEvent[5]; const x = (t / this.tickPreBeat) * this.config.beatWidth; const y = (127 - pitch) * this.config.noteHeight; const w = (duration / this.tickPreBeat) * this.config.beatWidth; const h = this.config.noteHeight; this.svgWidth = Math.ceil(Math.max(x + w, this.svgWidth)); const color = this.getColor(0, channel); const opacity = (Math.min(1, velocity / 127 + 0.1)).toFixed(2); const rect = this.drawNote(x, y, w, h, `rgba(${color.r}, ${color.g}, ${color.b}, ${opacity})`); midiEvent.push(rect); this.setPlayTime(t); this.wrapper.scrollTo(this.svgWidth - this.wrapper.offsetWidth, 0); } this.midiEvents.push(midiEvent); this.svg.style.width = `${this.svgWidth}px`; } } getColor(track, channel) { const colors = [ [255, 0, 0], // Red [255, 255, 0], // Yellow [0, 128, 0], // Green [0, 255, 255], // Cyan [0, 0, 255], // Blue [255, 192, 203], // Pink [255, 165, 0], // Orange [128, 0, 128], // Purple [128, 128, 128], // Gray [255, 255, 255], // White [255, 215, 0], // Gold [192, 192, 192], // Silver ]; // Calculate an index based on the track and channel const index = (track + channel) % colors.length; // Get the RGB values from the colors array const [r, g, b] = colors[index]; // Return the RGB color in the format "rgb(r, g, b)" return { r, g, b }; } drawNote(x, y, w, h, fill) { if (!this.svg) { return null; } const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); rect.classList.add('note'); rect.setAttribute('fill', fill); // Round values to the nearest integer to avoid partially filled pixels. rect.setAttribute('x', `${Math.round(x)}`); rect.setAttribute('y', `${Math.round(y)}`); rect.setAttribute('width', `${Math.round(w)}`); rect.setAttribute('height', `${Math.round(h)}`); this.svg.appendChild(rect); return rect; } finishAppendMidiEvent() { this.pause(); const midiEvents = this.midiEvents.sort((a, b) => a[1] - b[1]); let tempo = (60 / 120) * 10 ** 3; let ms = 0; let lastT = 0; this.midiTimes.push({ ms: ms, t: 0, tempo: tempo }); midiEvents.forEach((midiEvent) => { const t = midiEvent[1]; ms += ((t - lastT) / this.tickPreBeat) * tempo; if (midiEvent[0] === 'set_tempo') { tempo = midiEvent[2]; this.midiTimes.push({ ms: ms, t: t, tempo: tempo }); } lastT = t; }); } setPlayTime(t) { this.playTime = t; const x = Math.round((t / this.tickPreBeat) * this.config.beatWidth); this.timeLine.setAttribute('x1', `${x}`); this.timeLine.setAttribute('y1', '0'); this.timeLine.setAttribute('x2', `${x}`); this.timeLine.setAttribute('y2', `${this.config.noteHeight * 128}`); this.wrapper.scrollTo(Math.max(0, x - this.wrapper.offsetWidth / 2), 0); if (this.playing) { const activeNotes = []; this.removeActiveNotes(this.activeNotes); this.midiEvents.forEach((midiEvent) => { if (midiEvent[0] === 'note') { const time = midiEvent[1]; const duration = midiEvent[2]; const note = midiEvent[midiEvent.length - 1]; if (time <= this.playTime && time + duration >= this.playTime) { activeNotes.push(note); } } }); this.addActiveNotes(activeNotes); } } setPlayTimeMs(ms) { this.playTimeMs = ms; let playTime = 0; for (let i = 0; i < this.midiTimes.length; i++) { const midiTime = this.midiTimes[i]; if (midiTime.ms >= ms) { break; } playTime = midiTime.t + ((ms - midiTime.ms) * this.tickPreBeat) / midiTime.tempo; } this.setPlayTime(playTime); } addActiveNotes(notes) { notes.forEach((note) => { this.activeNotes.push(note); note.classList.add('active'); }); } removeActiveNotes(notes) { notes.forEach((note) => { const idx = this.activeNotes.indexOf(note); if (idx > -1) this.activeNotes.splice(idx, 1); note.classList.remove('active'); }); } play() { this.playing = true; this.timer = setInterval(() => { this.setPlayTimeMs(this.playTimeMs + 10); }, 10); } pause() { if (!!this.timer) clearInterval(this.timer); this.removeActiveNotes(this.activeNotes); this.timer = null; this.playing = false; } bindAudioPlayer(audio) { this.pause(); audio.addEventListener('play', (event) => { this.play(); }); audio.addEventListener('pause', (event) => { this.pause(); }); audio.addEventListener('timeupdate', (event) => { this.setPlayTimeMs(event.target.currentTime * 10 ** 3); }); } } customElements.define('midi-visualizer', MidiVisualizer); (function () { let midi_visualizer_container_inited = null; let midi_audio_inited = null; const midi_visualizer = document.createElement('midi-visualizer'); if (window.innerWidth < 300) { midi_visualizer.config.noteHeight = 1; midi_visualizer.config.beatWidth = 8; } else if (window.innerWidth < 600) { midi_visualizer.config.noteHeight = 2; midi_visualizer.config.beatWidth = 16; } else if (window.innerWidth < 1280) { midi_visualizer.config.noteHeight = 4; midi_visualizer.config.beatWidth = 32; } else { midi_visualizer.config.noteHeight = 4; midi_visualizer.config.beatWidth = 64; } midi_visualizer.svg.style.height = `${midi_visualizer.config.noteHeight * 128}px`; // Reload svg height onUiUpdate((m) => { const app = gradioApp(); const midi_visualizer_container = app.querySelector('#midi_visualizer_container'); if (!!midi_visualizer_container && midi_visualizer_container_inited !== midi_visualizer_container) { midi_visualizer_container.appendChild(midi_visualizer); midi_visualizer_container_inited = midi_visualizer_container; } const midi_audio = app.querySelector('#midi_audio > audio'); if (!!midi_audio && midi_audio_inited !== midi_audio) { midi_visualizer.bindAudioPlayer(midi_audio); midi_audio_inited = midi_audio; } }); function createProgressBar(progressbarContainer) { const parentProgressbar = progressbarContainer.parentNode; const divProgress = document.createElement('div'); divProgress.className = 'progressDiv'; const rect = progressbarContainer.getBoundingClientRect(); divProgress.style.width = rect.width + 'px'; divProgress.style.background = '#b4c0cc'; divProgress.style.borderRadius = '8px'; const divInner = document.createElement('div'); divInner.className = 'progress'; divInner.style.color = 'white'; divInner.style.background = '#0060df'; divInner.style.textAlign = 'right'; divInner.style.fontWeight = 'bold'; divInner.style.borderRadius = '8px'; divInner.style.height = '20px'; divInner.style.lineHeight = '20px'; divInner.style.paddingRight = '8px'; divInner.style.width = '0%'; divProgress.appendChild(divInner); parentProgressbar.insertBefore(divProgress, progressbarContainer); } function removeProgressBar(progressbarContainer) { const parentProgressbar = progressbarContainer.parentNode; const divProgress = parentProgressbar.querySelector('.progressDiv'); parentProgressbar.removeChild(divProgress); } function setProgressBar(progressbarContainer, progress, total) { const parentProgressbar = progressbarContainer.parentNode; const divProgress = parentProgressbar.querySelector('.progressDiv'); const divInner = parentProgressbar.querySelector('.progress'); if (total === 0) total = 1; divInner.style.width = `${(progress / total) * 100}%`; divInner.textContent = `${progress}/${total}`; } onMsgReceive((msg) => { switch (msg.name) { case 'visualizer_clear': midi_visualizer.clearMidiEvents(); createProgressBar(midi_visualizer_container_inited); break; case 'visualizer_append': midi_visualizer.appendMidiEvent(msg.data); break; case 'progress': const progress = msg.data[0]; const total = msg.data[1]; setProgressBar(midi_visualizer_container_inited, progress, total); break; case 'visualizer_end': midi_visualizer.finishAppendMidiEvent(); midi_visualizer.setPlayTime(0); removeProgressBar(midi_visualizer_container_inited); break; default: } }); })();