Major Issue Fix
Resolved issue preventing audio from playing in the background when sent from Silly Tavern. Resolved issue preventing API settings to not impact node such as volume and toggles.
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
.gitattributes
|
||||
.gitignore
|
||||
LICENSE
|
||||
README.md
|
||||
__init__.py
|
||||
js/auto_audio_player.js
|
||||
pyproject.toml
|
||||
+26
-8
@@ -1,4 +1,3 @@
|
||||
import io
|
||||
import os
|
||||
import uuid
|
||||
import wave
|
||||
@@ -14,6 +13,18 @@ WEB_DIRECTORY = "./js"
|
||||
|
||||
TEMP_SUBDIR = "auto_audio_player"
|
||||
|
||||
LATEST_AUDIO_STATE = {
|
||||
"revision": 0,
|
||||
"filename": None,
|
||||
"url": None,
|
||||
"sample_rate": None,
|
||||
"channels": None,
|
||||
"duration": None,
|
||||
"autoplay": True,
|
||||
"default_volume": 1.0,
|
||||
"loop": False,
|
||||
}
|
||||
|
||||
|
||||
def _get_temp_root() -> Path:
|
||||
base = Path(tempfile.gettempdir()) / "ComfyUI" / TEMP_SUBDIR
|
||||
@@ -36,28 +47,23 @@ def _to_numpy_audio(audio):
|
||||
else:
|
||||
data = np.asarray(waveform, dtype=np.float32)
|
||||
|
||||
# Common ComfyUI audio shapes include:
|
||||
# [batch, channels, samples], [channels, samples], [samples]
|
||||
if data.ndim == 3:
|
||||
data = data[0]
|
||||
if data.ndim == 1:
|
||||
data = data[np.newaxis, :]
|
||||
elif data.ndim == 2:
|
||||
# If someone hands in [samples, channels], rotate it.
|
||||
if data.shape[0] > data.shape[1] and data.shape[1] <= 8:
|
||||
data = data.T
|
||||
else:
|
||||
raise ValueError(f"Unsupported waveform shape: {data.shape}")
|
||||
|
||||
# Final shape: [channels, samples]
|
||||
data = np.clip(data, -1.0, 1.0)
|
||||
return data, sample_rate
|
||||
|
||||
|
||||
def _write_wav(audio_path: Path, waveform: np.ndarray, sample_rate: int):
|
||||
# Convert float32 [-1, 1] to 16-bit PCM.
|
||||
pcm = (waveform * 32767.0).astype(np.int16)
|
||||
pcm_interleaved = pcm.T # [samples, channels]
|
||||
pcm_interleaved = pcm.T
|
||||
|
||||
with wave.open(str(audio_path), "wb") as wav_file:
|
||||
wav_file.setnchannels(pcm.shape[0])
|
||||
@@ -78,6 +84,11 @@ async def serve_auto_audio_player_file(request):
|
||||
return web.FileResponse(path, headers={"Cache-Control": "no-store"})
|
||||
|
||||
|
||||
@PromptServer.instance.routes.get("/auto_audio_player/latest")
|
||||
async def get_latest_auto_audio_player_state(request):
|
||||
return web.json_response(LATEST_AUDIO_STATE, headers={"Cache-Control": "no-store"})
|
||||
|
||||
|
||||
class AutoAudioPlayer:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
@@ -97,6 +108,8 @@ class AutoAudioPlayer:
|
||||
OUTPUT_NODE = True
|
||||
|
||||
def process(self, audio, autoplay=True, default_volume=1.0, loop=False):
|
||||
global LATEST_AUDIO_STATE
|
||||
|
||||
waveform, sample_rate = _to_numpy_audio(audio)
|
||||
filename = f"auto_audio_{uuid.uuid4().hex}.wav"
|
||||
out_path = _get_temp_root() / filename
|
||||
@@ -115,6 +128,11 @@ class AutoAudioPlayer:
|
||||
"loop": bool(loop),
|
||||
}
|
||||
|
||||
LATEST_AUDIO_STATE = {
|
||||
"revision": int(LATEST_AUDIO_STATE.get("revision", 0)) + 1,
|
||||
**ui_payload,
|
||||
}
|
||||
|
||||
return {"ui": {"audio": [ui_payload]}, "result": (audio,)}
|
||||
|
||||
|
||||
@@ -126,4 +144,4 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"AutoAudioPlayer": "Auto Audio Player",
|
||||
}
|
||||
|
||||
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"]
|
||||
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"]
|
||||
+190
-72
@@ -1,6 +1,19 @@
|
||||
|
||||
import { app } from '../../scripts/app.js'
|
||||
|
||||
console.log('[AutoAudioPlayer] FILE LOADED polling build')
|
||||
|
||||
let autoAudioUnlocked = false
|
||||
const autoAudioNodeInstances = new Map()
|
||||
let latestSeenRevision = 0
|
||||
|
||||
window.addEventListener('unhandledrejection', (event) => {
|
||||
console.warn('[AutoAudioPlayer] global unhandled rejection:', event.reason)
|
||||
})
|
||||
|
||||
function logAAP(...args) {
|
||||
console.log('[AutoAudioPlayer]', ...args)
|
||||
}
|
||||
|
||||
function formatTime(seconds) {
|
||||
if (!Number.isFinite(seconds) || seconds < 0) return '0:00'
|
||||
const mins = Math.floor(seconds / 60)
|
||||
@@ -8,19 +21,6 @@ function formatTime(seconds) {
|
||||
return `${mins}:${String(secs).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
function getPayload(message) {
|
||||
const direct = Array.isArray(message?.audio) ? message.audio[0] : message?.audio
|
||||
if (direct?.url) return direct
|
||||
|
||||
const nestedUi = Array.isArray(message?.ui?.audio) ? message.ui.audio[0] : message?.ui?.audio
|
||||
if (nestedUi?.url) return nestedUi
|
||||
|
||||
const nestedResult = Array.isArray(message?.result?.ui?.audio) ? message.result.ui.audio[0] : message?.result?.ui?.audio
|
||||
if (nestedResult?.url) return nestedResult
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function styleRange(input) {
|
||||
input.style.width = '100%'
|
||||
input.style.margin = '0'
|
||||
@@ -53,10 +53,155 @@ function hideWidget(widget) {
|
||||
}
|
||||
}
|
||||
|
||||
async function unlockAudioPlayback() {
|
||||
if (autoAudioUnlocked) return true
|
||||
|
||||
try {
|
||||
const probe = document.createElement('audio')
|
||||
probe.muted = true
|
||||
probe.playsInline = true
|
||||
const result = probe.play()
|
||||
if (result && typeof result.then === 'function') {
|
||||
await result.catch(() => {})
|
||||
}
|
||||
probe.pause()
|
||||
} catch (_) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
autoAudioUnlocked = true
|
||||
logAAP('audio unlock attempted')
|
||||
return true
|
||||
}
|
||||
|
||||
async function safePlay(audioEl, statusEl, reason = 'unknown') {
|
||||
if (!audioEl?.src) {
|
||||
logAAP('safePlay skipped, no src', reason)
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
logAAP('safePlay attempting', reason, audioEl.src)
|
||||
const result = audioEl.play()
|
||||
if (result && typeof result.then === 'function') {
|
||||
await result
|
||||
}
|
||||
logAAP('safePlay success', reason)
|
||||
return true
|
||||
} catch (err) {
|
||||
console.warn('[AutoAudioPlayer] play() failed:', reason, err)
|
||||
if (statusEl && !statusEl.textContent.includes('Autoplay blocked')) {
|
||||
statusEl.textContent += ' • Autoplay blocked by browser until you click in the ComfyUI page once.'
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function applyPayloadToNode(node, payload, source = 'polling') {
|
||||
const ui = node?.__autoAudioPlayer
|
||||
if (!ui) return
|
||||
|
||||
if (!payload?.url) {
|
||||
ui.status.textContent = 'Execution finished, but no audio preview payload was returned.'
|
||||
logAAP('applyPayloadToNode no payload url', source, payload)
|
||||
return
|
||||
}
|
||||
|
||||
const cacheBustedUrl = `${payload.url}${payload.url.includes('?') ? '&' : '?'}t=${Date.now()}`
|
||||
logAAP('setting audio src', source, cacheBustedUrl)
|
||||
|
||||
const preferredVolume = Number.isFinite(Number(payload.default_volume))
|
||||
? Number(payload.default_volume)
|
||||
: Number.isFinite(Number(findWidget(node, 'default_volume')?.value))
|
||||
? Number(findWidget(node, 'default_volume')?.value)
|
||||
: Number(ui.volume.value ?? 1)
|
||||
|
||||
const preferredLoop = payload.loop ?? findWidget(node, 'loop')?.value ?? ui.loopToggle.checked
|
||||
const preferredAutoplay = payload.autoplay ?? findWidget(node, 'autoplay')?.value ?? ui.autoplayToggle.checked
|
||||
|
||||
setWidgetValue(findWidget(node, 'default_volume'), preferredVolume, node)
|
||||
setWidgetValue(findWidget(node, 'loop'), !!preferredLoop, node)
|
||||
setWidgetValue(findWidget(node, 'autoplay'), preferredAutoplay !== false, node)
|
||||
|
||||
ui.audio.pause()
|
||||
ui.audio.currentTime = 0
|
||||
ui.audio.src = cacheBustedUrl
|
||||
ui.audio.load()
|
||||
|
||||
ui.volume.value = String(preferredVolume)
|
||||
ui.audio.volume = preferredVolume
|
||||
|
||||
ui.loopToggle.checked = !!preferredLoop
|
||||
ui.audio.loop = ui.loopToggle.checked
|
||||
|
||||
ui.autoplayToggle.checked = preferredAutoplay !== false
|
||||
|
||||
const durationText = Number.isFinite(payload.duration) ? formatTime(payload.duration) : '0:00'
|
||||
ui.status.textContent = `Audio ready • ${payload.sample_rate ?? '?'} Hz • ${payload.channels ?? '?'} ch • ${durationText}`
|
||||
ui.syncButtonLabel()
|
||||
ui.syncProgress()
|
||||
ui.syncTimeLabel()
|
||||
|
||||
if (preferredAutoplay) {
|
||||
Promise.resolve()
|
||||
.then(async () => {
|
||||
logAAP('attempting autoplay', source)
|
||||
await safePlay(ui.audio, ui.status, `${source}-autoplay`)
|
||||
ui.syncButtonLabel()
|
||||
})
|
||||
.catch((err) => {
|
||||
console.warn('[AutoAudioPlayer] autoplay promise chain failed:', source, err)
|
||||
ui.syncButtonLabel()
|
||||
})
|
||||
}
|
||||
|
||||
node.setDirtyCanvas?.(true, true)
|
||||
}
|
||||
|
||||
async function pollLatestAudioState() {
|
||||
try {
|
||||
const response = await fetch('/auto_audio_player/latest', { cache: 'no-store' })
|
||||
if (!response.ok) return
|
||||
|
||||
const data = await response.json()
|
||||
const revision = Number(data?.revision ?? 0)
|
||||
|
||||
if (!revision || revision <= latestSeenRevision) return
|
||||
latestSeenRevision = revision
|
||||
|
||||
logAAP('poll hit new revision', revision, data)
|
||||
|
||||
for (const node of autoAudioNodeInstances.values()) {
|
||||
applyPayloadToNode(node, data, 'polling')
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[AutoAudioPlayer] polling failed:', err)
|
||||
}
|
||||
}
|
||||
|
||||
if (!window.__autoAudioPlayerUnlockBound) {
|
||||
window.__autoAudioPlayerUnlockBound = true
|
||||
|
||||
const unlockOnce = async () => {
|
||||
await unlockAudioPlayback()
|
||||
window.removeEventListener('pointerdown', unlockOnce, true)
|
||||
window.removeEventListener('keydown', unlockOnce, true)
|
||||
}
|
||||
|
||||
window.addEventListener('pointerdown', unlockOnce, true)
|
||||
window.addEventListener('keydown', unlockOnce, true)
|
||||
}
|
||||
|
||||
if (!window.__autoAudioPlayerPollingBound) {
|
||||
window.__autoAudioPlayerPollingBound = true
|
||||
setInterval(pollLatestAudioState, 1000)
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
name: 'custom.auto_audio_player',
|
||||
|
||||
async beforeRegisterNodeDef(nodeType, nodeData) {
|
||||
console.log('[AutoAudioPlayer] beforeRegisterNodeDef seen', nodeData?.name)
|
||||
const nodeName = nodeData?.name || nodeData?.display_name || nodeType?.comfyClass || nodeType?.type
|
||||
if (nodeName !== 'AutoAudioPlayer') return
|
||||
|
||||
@@ -66,6 +211,7 @@ app.registerExtension({
|
||||
|
||||
if (this.__autoAudioPlayerInitialized) return result
|
||||
this.__autoAudioPlayerInitialized = true
|
||||
logAAP('nodeCreated', this.id)
|
||||
|
||||
const autoplayWidget = findWidget(this, 'autoplay')
|
||||
const volumeWidget = findWidget(this, 'default_volume')
|
||||
@@ -106,6 +252,7 @@ app.registerExtension({
|
||||
|
||||
const audio = document.createElement('audio')
|
||||
audio.preload = 'auto'
|
||||
audio.playsInline = true
|
||||
audio.style.display = 'none'
|
||||
|
||||
const playRow = document.createElement('div')
|
||||
@@ -216,7 +363,6 @@ app.registerExtension({
|
||||
spacer.style.minWidth = '1px'
|
||||
|
||||
controlsGrid.append(volumeLabel, volume, loopWrap, autoplayLabel, autoplayToggleWrap, spacer)
|
||||
|
||||
container.append(status, playRow, controlsGrid, audio)
|
||||
|
||||
this.addDOMWidget('auto_audio_player', 'auto_audio_player', container)
|
||||
@@ -246,9 +392,10 @@ app.registerExtension({
|
||||
if (!audio.src) return
|
||||
try {
|
||||
if (audio.paused) {
|
||||
await audio.play()
|
||||
await safePlay(audio, status, 'manual-button')
|
||||
} else {
|
||||
audio.pause()
|
||||
logAAP('manual pause')
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[AutoAudioPlayer] play/pause error:', err)
|
||||
@@ -267,32 +414,46 @@ app.registerExtension({
|
||||
const value = Number(volume.value)
|
||||
audio.volume = value
|
||||
setWidgetValue(volumeWidget, value, this)
|
||||
logAAP('volume changed', value)
|
||||
}
|
||||
|
||||
loopToggle.onchange = () => {
|
||||
audio.loop = loopToggle.checked
|
||||
setWidgetValue(loopWidget, loopToggle.checked, this)
|
||||
logAAP('loop changed', loopToggle.checked)
|
||||
}
|
||||
|
||||
autoplayToggle.onchange = () => {
|
||||
setWidgetValue(autoplayWidget, autoplayToggle.checked, this)
|
||||
logAAP('autoplay changed', autoplayToggle.checked)
|
||||
}
|
||||
|
||||
audio.addEventListener('play', syncButtonLabel)
|
||||
audio.addEventListener('pause', syncButtonLabel)
|
||||
audio.addEventListener('play', () => {
|
||||
logAAP('audio event: play')
|
||||
syncButtonLabel()
|
||||
})
|
||||
audio.addEventListener('pause', () => {
|
||||
logAAP('audio event: pause')
|
||||
syncButtonLabel()
|
||||
})
|
||||
audio.addEventListener('timeupdate', () => {
|
||||
syncProgress()
|
||||
syncTimeLabel()
|
||||
})
|
||||
audio.addEventListener('loadedmetadata', () => {
|
||||
logAAP('audio event: loadedmetadata', audio.duration)
|
||||
syncProgress()
|
||||
syncTimeLabel()
|
||||
})
|
||||
audio.addEventListener('ended', () => {
|
||||
logAAP('audio event: ended')
|
||||
syncButtonLabel()
|
||||
syncProgress()
|
||||
syncTimeLabel()
|
||||
})
|
||||
audio.addEventListener('error', () => {
|
||||
logAAP('audio event: error', audio.error)
|
||||
})
|
||||
|
||||
this.__autoAudioPlayer = {
|
||||
status,
|
||||
@@ -306,60 +467,17 @@ app.registerExtension({
|
||||
syncTimeLabel,
|
||||
}
|
||||
|
||||
autoAudioNodeInstances.set(String(this.id), this)
|
||||
logAAP('registered node instance', this.id)
|
||||
|
||||
const onRemoved = this.onRemoved
|
||||
this.onRemoved = function () {
|
||||
autoAudioNodeInstances.delete(String(this.id))
|
||||
logAAP('removed node instance', this.id)
|
||||
return onRemoved?.apply(this, arguments)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
const onExecuted = nodeType.prototype.onExecuted
|
||||
nodeType.prototype.onExecuted = function (message) {
|
||||
onExecuted?.apply(this, arguments)
|
||||
|
||||
const ui = this.__autoAudioPlayer
|
||||
if (!ui) return
|
||||
|
||||
const payload = getPayload(message)
|
||||
if (!payload?.url) {
|
||||
ui.status.textContent = 'Execution finished, but no audio preview payload was returned.'
|
||||
console.warn('[AutoAudioPlayer] No usable payload found', message)
|
||||
return
|
||||
}
|
||||
|
||||
const cacheBustedUrl = `${payload.url}${payload.url.includes('?') ? '&' : '?'}t=${Date.now()}`
|
||||
const preferredVolume = Number.isFinite(Number(findWidget(this, 'default_volume')?.value))
|
||||
? Number(findWidget(this, 'default_volume')?.value)
|
||||
: Number.isFinite(Number(ui.volume.value))
|
||||
? Number(ui.volume.value)
|
||||
: Number(payload.default_volume ?? 1)
|
||||
|
||||
const preferredLoop = findWidget(this, 'loop')?.value ?? payload.loop
|
||||
const preferredAutoplay = findWidget(this, 'autoplay')?.value ?? payload.autoplay
|
||||
|
||||
ui.audio.pause()
|
||||
ui.audio.src = cacheBustedUrl
|
||||
ui.audio.load()
|
||||
|
||||
ui.volume.value = String(preferredVolume)
|
||||
ui.audio.volume = preferredVolume
|
||||
|
||||
ui.loopToggle.checked = !!preferredLoop
|
||||
ui.audio.loop = ui.loopToggle.checked
|
||||
|
||||
ui.autoplayToggle.checked = preferredAutoplay !== false
|
||||
|
||||
const durationText = Number.isFinite(payload.duration) ? formatTime(payload.duration) : '0:00'
|
||||
ui.status.textContent = `Audio ready • ${payload.sample_rate ?? '?'} Hz • ${payload.channels ?? '?'} ch • ${durationText}`
|
||||
ui.syncButtonLabel()
|
||||
ui.syncProgress()
|
||||
ui.syncTimeLabel()
|
||||
|
||||
if (preferredAutoplay) {
|
||||
ui.audio.play().catch((err) => {
|
||||
console.warn('[AutoAudioPlayer] Autoplay blocked or failed:', err)
|
||||
ui.status.textContent += ' • Autoplay blocked by browser until you interact with the page.'
|
||||
ui.syncButtonLabel()
|
||||
})
|
||||
}
|
||||
|
||||
this.setDirtyCanvas(true, true)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "auto-audio-player"
|
||||
description = ""
|
||||
version = "1.0.0"
|
||||
description = "An auto audio player node for ComyUI. Offers a looping toggle, autoplay toggle, scrubbing, volume control, pause and play. Once audio is pushed into the node it will automatically play."
|
||||
version = "1.1.0"
|
||||
license = "MIT"
|
||||
# classifiers = [
|
||||
# # For OS-independent nodes (works on all operating systems)
|
||||
|
||||
Reference in New Issue
Block a user