|
import { app } from '../../../scripts/app.js' |
|
import { api } from '../../../scripts/api.js' |
|
import { ComfyWidgets } from '../../../scripts/widgets.js' |
|
import { $el } from '../../../scripts/ui.js' |
|
|
|
import WaveSurfer from 'https://cdn.jsdelivr.net/npm/wavesurfer.js@7/dist/wavesurfer.esm.js' |
|
|
|
function get_position_style (ctx, widget_width, y, node_height) { |
|
const MARGIN = 4 |
|
|
|
|
|
const elRect = ctx.canvas.getBoundingClientRect() |
|
const transform = new DOMMatrix() |
|
.scaleSelf( |
|
elRect.width / ctx.canvas.width, |
|
elRect.height / ctx.canvas.height |
|
) |
|
.multiplySelf(ctx.getTransform()) |
|
.translateSelf(MARGIN, MARGIN + y) |
|
|
|
return { |
|
transformOrigin: '0 0', |
|
transform: transform, |
|
left: `0`, |
|
top: '0', |
|
cursor: 'pointer', |
|
position: 'absolute', |
|
maxWidth: `${widget_width - MARGIN * 2}px`, |
|
|
|
width: `${widget_width - MARGIN * 2}px`, |
|
|
|
|
|
display: 'flex', |
|
flexDirection: 'column', |
|
|
|
justifyContent: 'space-around' |
|
} |
|
} |
|
|
|
|
|
const parseUrl = data => { |
|
let { filename, subfolder, type, prompt } = data |
|
return { |
|
url: api.apiURL( |
|
`/view?filename=${encodeURIComponent( |
|
filename |
|
)}&type=${type}&subfolder=${subfolder}${app.getPreviewFormatParam()}${app.getRandParam()}` |
|
), |
|
prompt |
|
} |
|
} |
|
|
|
const createWaveSurfer = (wavesurfer, id,url) => { |
|
|
|
if (wavesurfer) { |
|
wavesurfer.destroy() |
|
} |
|
wavesurfer = WaveSurfer.create({ |
|
container: '#' + id, |
|
waveColor: 'rgb(200, 0, 200)', |
|
progressColor: 'rgb(100, 0, 100)', |
|
|
|
barWidth: 10, |
|
|
|
barGap: 2, |
|
|
|
barRadius: 6, |
|
url |
|
}) |
|
|
|
wavesurfer._auto = true |
|
|
|
|
|
wavesurfer.on('finish', function () { |
|
|
|
if (wavesurfer._auto) wavesurfer.play() |
|
}) |
|
|
|
wavesurfer.on('interaction', () => { |
|
wavesurfer._auto = false |
|
if (!wavesurfer.isPlaying()) wavesurfer.play() |
|
}) |
|
|
|
|
|
wavesurfer.on('audioprocess', () => { |
|
if (wavesurfer.isPlaying()&&wavesurfer.getDecodedData()) { |
|
const channelData = wavesurfer.getDecodedData().getChannelData(0); |
|
const currentTime = wavesurfer.getCurrentTime() |
|
|
|
const sampleRate = wavesurfer.getDecodedData().sampleRate |
|
|
|
|
|
const windowSize = 1 |
|
const startSample = Math.floor(currentTime * sampleRate) |
|
const endSample = Math.min( |
|
startSample + windowSize * sampleRate, |
|
channelData.length |
|
) |
|
|
|
let peak = 0 |
|
for (let i = startSample; i < endSample; i++) { |
|
const value = Math.abs(channelData[i]) |
|
if (value > peak) { |
|
peak = value |
|
} |
|
} |
|
|
|
} |
|
}) |
|
|
|
return wavesurfer |
|
} |
|
|
|
|
|
function updateWaveWidgetValue (widgets, id, url, prompt, wavesurfer) { |
|
let widget = widgets.filter(w => w.name == 'AudioPlay')[0] |
|
|
|
widget.value = [url, prompt] |
|
|
|
if (widget.div) { |
|
widget.div.querySelector('.wave').id = `AudioPlay_${id}` |
|
} |
|
|
|
wavesurfer = createWaveSurfer(wavesurfer, `AudioPlay_${id}`,url) |
|
|
|
wavesurfer.on('ready', duration => { |
|
console.log('Audio duration: ' + duration + ' seconds') |
|
if (widget.div) { |
|
widget.div.setAttribute('data-url', url) |
|
widget.div.querySelector('.link').setAttribute('href', url) |
|
widget.div.querySelector( |
|
'.info' |
|
).innerHTML = `<span style="font-size: 12px; |
|
margin: 8px;">${duration.toFixed( |
|
2 |
|
)} seconds</span> <br><span style="font-size: 14px;">${prompt||''}</span> <br>` |
|
} |
|
}) |
|
|
|
|
|
wavesurfer.load(url) |
|
|
|
return wavesurfer |
|
} |
|
|
|
app.registerExtension({ |
|
name: 'SoundLab.AudioPlay', |
|
async beforeRegisterNodeDef (nodeType, nodeData, app) { |
|
if (nodeType.comfyClass == 'AudioPlay') { |
|
let that = this |
|
|
|
|
|
const orig_nodeCreated = nodeType.prototype.onNodeCreated |
|
nodeType.prototype.onNodeCreated = function () { |
|
orig_nodeCreated?.apply(this, arguments) |
|
|
|
const widget = { |
|
type: 'div', |
|
name: 'AudioPlay', |
|
draw (ctx, node, widget_width, y, widget_height) { |
|
Object.assign( |
|
this.div.style, |
|
get_position_style(ctx, widget_width, y, node.size[1]) |
|
) |
|
} |
|
} |
|
|
|
|
|
widget.div = $el('div', {}) |
|
|
|
document.body.appendChild(widget.div) |
|
|
|
|
|
const waveDiv = document.createElement('div') |
|
waveDiv.className = 'wave' |
|
waveDiv.style.minHeight = '172px' |
|
widget.div.appendChild(waveDiv) |
|
|
|
|
|
const infoDiv = document.createElement('div') |
|
infoDiv.className = 'info' |
|
infoDiv.style.marginBottom = '20px' |
|
widget.div.appendChild(infoDiv) |
|
|
|
|
|
let btns = document.createElement('div') |
|
btns.className = 'btns' |
|
btns.style = `display: flex; |
|
width: 100%; |
|
justify-content: space-between;` |
|
widget.div.appendChild(btns) |
|
|
|
|
|
const playBtn = document.createElement('a') |
|
playBtn.innerText = 'Play/Pause' |
|
|
|
playBtn.style = ` |
|
display: flex; |
|
padding: 4px 15px; |
|
background-color: var(--comfy-input-bg); |
|
border-radius: 8px; |
|
border-color: var(--border-color); |
|
border-style: solid; |
|
color: var(--descrip-text); |
|
text-decoration: none; |
|
border-radius: 5px; |
|
transition: background-color 0.3s ease 0s; |
|
` |
|
|
|
playBtn.addEventListener('click', e => { |
|
e.preventDefault() |
|
if (that[`wavesurfer_${this.id}`]) { |
|
that[`wavesurfer_${this.id}`]?.playPause() |
|
that[`wavesurfer_${this.id}`]._auto = true |
|
} |
|
}) |
|
btns.appendChild(playBtn) |
|
|
|
const urlLink = document.createElement('a') |
|
urlLink.className = 'link' |
|
urlLink.innerText = 'URL' |
|
urlLink.setAttribute('target', '_blank') |
|
urlLink.style = `display: flex; |
|
padding: 4px 15px; |
|
background-color: var(--comfy-input-bg); |
|
border-radius: 8px; |
|
border-color: var(--border-color); |
|
border-style: solid; |
|
color: var(--descrip-text); |
|
text-decoration: none; |
|
border-radius: 5px; |
|
transition: background-color 0.3s ease 0s;` |
|
|
|
btns.appendChild(urlLink) |
|
|
|
|
|
|
|
|
|
|
|
|
|
this.addCustomWidget(widget) |
|
|
|
const onRemoved = this.onRemoved |
|
this.onRemoved = () => { |
|
widget.div.remove() |
|
return onRemoved?.() |
|
} |
|
|
|
this.size = [this.size[0], 280] |
|
this.serialize_widgets = true |
|
} |
|
|
|
const onExecuted = nodeType.prototype.onExecuted |
|
nodeType.prototype.onExecuted = function (message) { |
|
onExecuted?.apply(this, arguments) |
|
const audio = message.audio |
|
console.log('#onExecuted', `AudioPlay_${this.id}`, message,audio) |
|
try { |
|
let { url, prompt } = parseUrl(audio[0]) |
|
|
|
that[`wavesurfer_${this.id}`] = updateWaveWidgetValue( |
|
this.widgets, |
|
this.id, |
|
url, |
|
prompt, |
|
that[`wavesurfer_${this.id}`] |
|
) |
|
|
|
that[`wavesurfer_${this.id}`]?.playPause() |
|
} catch (error) { |
|
console.log(error) |
|
} |
|
} |
|
} |
|
}, |
|
async loadedGraphNode (node, app) { |
|
if (node.type === 'AudioPlay') { |
|
let widget = node.widgets.filter(w => w.name == 'AudioPlay')[0] |
|
|
|
if (widget.value) { |
|
let [url, prompt] = widget.value |
|
|
|
this[`wavesurfer_${node.id}`] = updateWaveWidgetValue( |
|
node.widgets, |
|
node.id, |
|
url, |
|
prompt, |
|
this[`wavesurfer_${node.id}`] |
|
) |
|
} |
|
|
|
console.log('#loadedGraphNode', node) |
|
} |
|
} |
|
}) |
|
|