Spaces:
Sleeping
Sleeping
| 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: | |
| } | |
| }); | |
| })(); |