Spaces:
Sleeping
Sleeping
Download ui/player.js from k-l-lambda/speaking-midi: direct link, hf CLI and curl.
- Browser
- Download file 6.47 kB
-
https://huggingface.co/spaces/k-l-lambda/speaking-midi/resolve/main/ui/player.js
- Command line
-
hf download hf://spaces/k-l-lambda/speaking-midi/ui/player.js
-
curl -L -o player.js https://huggingface.co/spaces/k-l-lambda/speaking-midi/resolve/main/ui/player.js
6.47 kB
| // Interaction reference: klstudio/app/views/midi-player.vue. | |
| // The renderer's WAV is the playback clock; notes come from the exported MIDI. | |
| let dispose = () => {}; | |
| function mount() { | |
| dispose(); | |
| const root = element.querySelector('.player'); | |
| if (!root) return; | |
| const $ = (s) => root.querySelector(s); | |
| const canvas = $('.roll'), ctx = canvas.getContext('2d'); | |
| const play = $('.play'), seek = $('.seek'), clock = $('.clock'); | |
| const audio = new Audio(); | |
| const listeners = new AbortController(); | |
| let frame = 0, stopped = false, data = null, offset = 0, zoom = 1; | |
| let width = 600, height = 300, low = 36, high = 84; | |
| try { data = props.value ? JSON.parse(props.value) : null; } catch (_) {} | |
| const duration = data ? data.duration : 8; | |
| const notes = data ? data.notes : []; | |
| const left = 46, top = 23, bottom = 10; | |
| const clamp = (n, a, b) => Math.min(b, Math.max(a, n)); | |
| const format = (n) => `${Math.floor(n / 60)}:${Math.floor(n % 60).toString().padStart(2, '0')}`; | |
| const on = (target, type, fn) => target.addEventListener(type, fn, {signal: listeners.signal}); | |
| const span = () => Math.max(duration / zoom, .1); | |
| function follow() { | |
| const time = audio.currentTime; | |
| if (time < offset + span() * .15 || time > offset + span() * .7) | |
| offset = clamp(time - span() * .4, 0, Math.max(0, duration - span())); | |
| } | |
| function draw() { | |
| if (stopped) return; | |
| const time = audio.currentTime; | |
| const scale = (width-left)/span(), row = (height-top-bottom)/(high-low+1); | |
| ctx.clearRect(0,0,width,height); | |
| ctx.fillStyle='#101b2d';ctx.fillRect(0,0,width,height); | |
| const playing = new Set(notes.filter(n=>!audio.paused && n.start<=time && n.start+n.duration>time).map(n=>n.pitch)); | |
| for (let pitch=low;pitch<=high;pitch++) { | |
| const y=top+(high-pitch)*row, black=[1,3,6,8,10].includes(pitch%12); | |
| ctx.fillStyle=black?'#0c1626':'#142136';ctx.fillRect(left,y,width-left,row); | |
| ctx.fillStyle=playing.has(pitch)?'#6ce2b6':black?'#23334a':'#e1e8f2';ctx.fillRect(0,y,left-2,Math.max(.5,row-1)); | |
| if (pitch%12===0) { | |
| ctx.fillStyle=playing.has(pitch)?'#102d28':'#4f627a';ctx.font='10px system-ui';ctx.fillText(`C${Math.floor(pitch/12)-1}`,7,y+Math.max(8,row*.8)); | |
| ctx.strokeStyle='#33425b';ctx.beginPath();ctx.moveTo(left,y+row);ctx.lineTo(width,y+row);ctx.stroke(); | |
| } | |
| } | |
| const step=span()>6?1:.5; | |
| for(let t=Math.ceil(offset/step)*step;t<=offset+span();t+=step) { | |
| const x=left+(t-offset)*scale; | |
| ctx.strokeStyle='#26364d';ctx.beginPath();ctx.moveTo(x,top);ctx.lineTo(x,height-bottom);ctx.stroke(); | |
| ctx.fillStyle='#9bb0ce';ctx.font='10px system-ui';ctx.fillText(`${t.toFixed(step===1?0:1)}s`,x+3,14); | |
| } | |
| ctx.save();ctx.beginPath();ctx.rect(left,top,width-left,height-top);ctx.clip(); | |
| for(const n of notes) { | |
| const x=left+(n.start-offset)*scale,y=top+(high-n.pitch)*row; | |
| const active=!audio.paused && n.start<=time && n.start+n.duration>time; | |
| ctx.fillStyle=active?'#d2ffe6':`hsl(${200-n.velocity*.35} 70% ${42+n.velocity*.15}%)`; | |
| ctx.fillRect(x,y+Math.min(1,row*.1),Math.max(2,n.duration*scale),Math.max(1,row-1)); | |
| if(active){ctx.strokeStyle='#6ce2b6';ctx.strokeRect(x,y,Math.max(2,n.duration*scale),row);} | |
| } | |
| const cursor=left+(time-offset)*scale; | |
| ctx.strokeStyle='#f8bc68';ctx.lineWidth=2;ctx.beginPath();ctx.moveTo(cursor,top);ctx.lineTo(cursor,height);ctx.stroke();ctx.lineWidth=1;ctx.restore(); | |
| canvas.dataset.time=String(time);canvas.dataset.offset=String(offset);canvas.dataset.active=String(playing.size); | |
| clock.textContent=`${format(time)} / ${format(duration)}`; | |
| seek.value=String(time); | |
| } | |
| function animate() { | |
| if(stopped || !element.isConnected) { dispose();return; } | |
| follow();draw(); | |
| if(!audio.paused) frame=requestAnimationFrame(animate); | |
| } | |
| function seekTo(time) {if(!data)return;audio.currentTime=clamp(time,0,duration);follow();draw();} | |
| function resize() { | |
| width=Math.max(180,canvas.clientWidth);const dpr=window.devicePixelRatio||1; | |
| canvas.width=width*dpr;canvas.height=height*dpr;ctx.setTransform(dpr,0,0,dpr,0,0);draw(); | |
| } | |
| play.textContent='▶ Play';play.setAttribute('aria-label','Play piano MIDI'); | |
| play.disabled=true;$('.restart').disabled=true;seek.disabled=true;seek.value='0'; | |
| $('.empty').hidden=false;$('.note-count').textContent='No MIDI loaded'; | |
| $('.speed').value='1';$('.zoom').value='1';$('.error').textContent=''; | |
| if(data) { | |
| if(notes.length){low=Math.max(0,Math.min(...notes.map(n=>n.pitch))-2);high=Math.min(127,Math.max(...notes.map(n=>n.pitch))+2);} | |
| audio.src=data.audio;audio.preload='auto';seek.max=String(duration); | |
| $('.empty').hidden=true;play.disabled=false;$('.restart').disabled=false;seek.disabled=false; | |
| $('.note-count').textContent=`${notes.length} notes`; | |
| } | |
| on(play,'click',async()=>{ | |
| if(!data)return; | |
| if(audio.paused) { | |
| if(audio.ended || audio.currentTime>=duration)seekTo(0); | |
| try{await audio.play();$('.error').textContent='';}catch(_){$('.error').textContent='Playback could not start. Please press Play again.';} | |
| } else audio.pause(); | |
| }); | |
| on(audio,'play',()=>{play.textContent='Ⅱ Pause';play.setAttribute('aria-label','Pause piano MIDI');cancelAnimationFrame(frame);animate();}); | |
| on(audio,'pause',()=>{play.textContent='▶ Play';play.setAttribute('aria-label','Play piano MIDI');cancelAnimationFrame(frame);draw();}); | |
| on(audio,'ended',()=>{cancelAnimationFrame(frame);draw();}); | |
| on(audio,'error',()=>{$('.error').textContent='Unable to load the piano audio.';}); | |
| on($('.restart'),'click',()=>seekTo(0)); | |
| on(seek,'input',()=>seekTo(Number(seek.value))); | |
| on($('.speed'),'change',(e)=>{audio.playbackRate=Number(e.target.value);}); | |
| on($('.zoom'),'input',(e)=>{zoom=Number(e.target.value);offset=clamp(audio.currentTime-span()*.4,0,Math.max(0,duration-span()));draw();}); | |
| on(canvas,'click',(e)=>{const x=e.clientX-canvas.getBoundingClientRect().left;if(x>=left)seekTo(offset+(x-left)/(width-left)*span());}); | |
| on(canvas,'keydown',(e)=>{if(['ArrowLeft','ArrowRight'].includes(e.key)){e.preventDefault();seekTo(audio.currentTime+(e.key==='ArrowRight'?1:-1));}}); | |
| const observer=new ResizeObserver(resize);observer.observe(canvas);resize(); | |
| dispose=()=>{if(stopped)return;stopped=true;listeners.abort();cancelAnimationFrame(frame);observer.disconnect();audio.pause();audio.removeAttribute('src');audio.load();}; | |
| } | |
| watch('value', mount); | |
| mount(); | |