UNPKG

@konsumer/nuked

Version:

Yamaha OPL2/3 FM synth chip emulator

130 lines (113 loc) 3.69 kB
// this is a simple web-component to encapsulate loading a simple little player import * as nuke from '@konsumer/nuked' import pako from 'pako' const basename = (f) => f.split('/').pop() // VGZ is just compressed VGM const vgzHandler = b => nuke.vgm(pako.ungzip(b)) class NukedPlayer extends HTMLElement { static get observedAttributes() { return ['src'] } constructor() { super() const shadow = this.attachShadow({ mode: 'open' }) shadow.innerHTML = ` <style> div { display: flex; align-items: center; gap: 4px; padding: 4px; font-size: 24px; } button { background: none; color: inherit; border: none; padding: 0; font: inherit; cursor: pointer; outline: inherit; } </style> <div> <button>▶️</button> <button>⏸️</button> <button>⏹️</button> <input type="range" value="0" step="0.01" /> <span>0.00</span> / <span>0.00</span> : <span></span> </div> ` const [buttonPlay, buttonPause, buttonStop] = shadow.querySelectorAll('button') const timeSlider = shadow.querySelector('input') const [timeCurrent, timeTotal, nameSpace] = shadow.querySelectorAll('span') this.timeSlider = timeSlider this.timeTotal = timeTotal this.nameSpace = nameSpace this.sliding = false timeSlider.addEventListener('mousedown', () => { this.sliding = true }) timeSlider.addEventListener('mouseup', () => { this.sliding = false if (this.opl) { this.opl.seek(parseFloat(timeSlider.value)) } }) shadow.addEventListener('click', async () => { const ctx = new AudioContext() if (ctx.state === 'suspended') { await ctx.resume() } if (!this.opl && this.soundq) { this.opl = await nuke.createAudioWorklet(ctx, this.soundq) delete this.soundq this.opl.connect(ctx.destination) this.opl.addEventListener('time', ({ current, total }) => { if (!this.sliding) { timeSlider.value = timeCurrent.innerText = current.toFixed(2) timeSlider.max = timeTotal.innerText = total.toFixed(2) } }) } }) buttonPlay.addEventListener('click', () => { setTimeout(() => this?.opl?.play && this.opl.play(), 100) }) buttonPause.addEventListener('click', () => { setTimeout(() => this?.opl?.pause && this.opl.pause(), 100) }) buttonStop.addEventListener('click', () => { setTimeout(() => this?.opl?.stop && this.opl.stop(), 100) }) } attributeChangedCallback(name, oldValue, newValue) { if (name === 'src' && newValue) { let parser if (newValue.toLowerCase().endsWith('.imf')) { parser = nuke.imf } else if (newValue.toLowerCase().endsWith('.dro')) { parser = nuke.dro } else if (newValue.toLowerCase().endsWith('.vgm')) { parser = nuke.vgm } else if (newValue.toLowerCase().endsWith('.vgz')) { parser = vgzHandler } else { parser = nuke.raw } if (parser) { fetch(newValue) .then((r) => r.arrayBuffer()) .then((bytes) => { this.nameSpace.innerText = basename(newValue) this.soundq = parser(bytes) // hack to get total before audio-context can start const total = nuke.getTimeLength(this.soundq) this.timeSlider.max = this.timeTotal.innerText = total.toFixed(2) }) } } } } customElements.define('nuked-player', NukedPlayer)