v1 Auto Audio Player Node
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,66 @@
|
||||
# ComfyUI Auto Audio Player
|
||||
|
||||
A lightweight ComfyUI custom node that automatically plays audio when a new `AUDIO` input reaches the node.
|
||||
|
||||
## Features
|
||||
|
||||
- Auto-play when fresh audio reaches the node
|
||||
- Play / Pause control
|
||||
- Seek bar for scrubbing through the clip
|
||||
- Volume control
|
||||
- Loop toggle
|
||||
- Autoplay toggle
|
||||
- Passes the original `AUDIO` through unchanged for downstream nodes
|
||||
|
||||
## Node
|
||||
|
||||
- **Name:** `Auto Audio Player`
|
||||
- **Category:** `audio/utils`
|
||||
|
||||
## Inputs
|
||||
|
||||
- `audio` (`AUDIO`)
|
||||
- `autoplay` (`BOOLEAN`, default `true`)
|
||||
- `default_volume` (`FLOAT`, default `1.0`)
|
||||
- `loop` (`BOOLEAN`, default `false`)
|
||||
|
||||
## Output
|
||||
|
||||
- `audio` (`AUDIO`) passthrough
|
||||
|
||||
## Install
|
||||
|
||||
1. Clone or download this repository.
|
||||
2. Place the folder in your ComfyUI `custom_nodes` directory.
|
||||
3. Restart ComfyUI.
|
||||
4. Add **Auto Audio Player** from `audio/utils`.
|
||||
5. Connect any `AUDIO` output into it.
|
||||
|
||||
Example:
|
||||
|
||||
```text
|
||||
Audio Source -> Auto Audio Player -> Any downstream AUDIO node
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- The node writes a temporary WAV file so the frontend can load and play it.
|
||||
- Some browsers block autoplay until the page has received user interaction. In that case, the node still loads the clip and the Play button works normally.
|
||||
- The custom UI hides the duplicate built-in control widgets and keeps playback controls in a single panel.
|
||||
- Frontend code uses ComfyUI's extension registration pattern with `WEB_DIRECTORY = "./js"` and a frontend script loaded from `js/auto_audio_player.js`.
|
||||
|
||||
## Files
|
||||
|
||||
```text
|
||||
ComfyUI-AutoAudioPlayer/
|
||||
├── __init__.py
|
||||
├── LICENSE
|
||||
├── README.md
|
||||
├── .gitignore
|
||||
└── js/
|
||||
└── auto_audio_player.js
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
@@ -0,0 +1,129 @@
|
||||
import io
|
||||
import os
|
||||
import uuid
|
||||
import wave
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
from aiohttp import web
|
||||
from server import PromptServer
|
||||
|
||||
WEB_DIRECTORY = "./js"
|
||||
|
||||
TEMP_SUBDIR = "auto_audio_player"
|
||||
|
||||
|
||||
def _get_temp_root() -> Path:
|
||||
base = Path(tempfile.gettempdir()) / "ComfyUI" / TEMP_SUBDIR
|
||||
base.mkdir(parents=True, exist_ok=True)
|
||||
return base
|
||||
|
||||
|
||||
def _to_numpy_audio(audio):
|
||||
if not isinstance(audio, dict):
|
||||
raise TypeError("Expected ComfyUI AUDIO input as a dict with waveform and sample_rate.")
|
||||
|
||||
waveform = audio.get("waveform")
|
||||
sample_rate = int(audio.get("sample_rate", 44100))
|
||||
|
||||
if waveform is None:
|
||||
raise ValueError("Audio input is missing 'waveform'.")
|
||||
|
||||
if isinstance(waveform, torch.Tensor):
|
||||
data = waveform.detach().cpu().float().numpy()
|
||||
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]
|
||||
|
||||
with wave.open(str(audio_path), "wb") as wav_file:
|
||||
wav_file.setnchannels(pcm.shape[0])
|
||||
wav_file.setsampwidth(2)
|
||||
wav_file.setframerate(sample_rate)
|
||||
wav_file.writeframes(pcm_interleaved.tobytes())
|
||||
|
||||
|
||||
@PromptServer.instance.routes.get("/auto_audio_player/{filename}")
|
||||
async def serve_auto_audio_player_file(request):
|
||||
filename = request.match_info["filename"]
|
||||
safe_name = os.path.basename(filename)
|
||||
path = _get_temp_root() / safe_name
|
||||
|
||||
if not path.exists() or not path.is_file():
|
||||
return web.Response(status=404, text="Audio file not found.")
|
||||
|
||||
return web.FileResponse(path, headers={"Cache-Control": "no-store"})
|
||||
|
||||
|
||||
class AutoAudioPlayer:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"audio": ("AUDIO",),
|
||||
"autoplay": ("BOOLEAN", {"default": True}),
|
||||
"default_volume": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
"loop": ("BOOLEAN", {"default": False}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("AUDIO",)
|
||||
RETURN_NAMES = ("audio",)
|
||||
FUNCTION = "process"
|
||||
CATEGORY = "audio/utils"
|
||||
OUTPUT_NODE = True
|
||||
|
||||
def process(self, audio, autoplay=True, default_volume=1.0, loop=False):
|
||||
waveform, sample_rate = _to_numpy_audio(audio)
|
||||
filename = f"auto_audio_{uuid.uuid4().hex}.wav"
|
||||
out_path = _get_temp_root() / filename
|
||||
_write_wav(out_path, waveform, sample_rate)
|
||||
|
||||
duration = float(waveform.shape[1]) / float(sample_rate) if sample_rate > 0 else 0.0
|
||||
|
||||
ui_payload = {
|
||||
"filename": filename,
|
||||
"url": f"/auto_audio_player/{filename}",
|
||||
"sample_rate": sample_rate,
|
||||
"channels": int(waveform.shape[0]),
|
||||
"duration": duration,
|
||||
"autoplay": bool(autoplay),
|
||||
"default_volume": float(default_volume),
|
||||
"loop": bool(loop),
|
||||
}
|
||||
|
||||
return {"ui": {"audio": [ui_payload]}, "result": (audio,)}
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"AutoAudioPlayer": AutoAudioPlayer,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"AutoAudioPlayer": "Auto Audio Player",
|
||||
}
|
||||
|
||||
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"]
|
||||
@@ -0,0 +1,365 @@
|
||||
|
||||
import { app } from '../../scripts/app.js'
|
||||
|
||||
function formatTime(seconds) {
|
||||
if (!Number.isFinite(seconds) || seconds < 0) return '0:00'
|
||||
const mins = Math.floor(seconds / 60)
|
||||
const secs = Math.floor(seconds % 60)
|
||||
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'
|
||||
input.style.minWidth = '0'
|
||||
input.style.accentColor = '#b866ff'
|
||||
}
|
||||
|
||||
function findWidget(node, name) {
|
||||
return node.widgets?.find((w) => w?.name === name) || null
|
||||
}
|
||||
|
||||
function setWidgetValue(widget, value, node) {
|
||||
if (!widget) return
|
||||
widget.value = value
|
||||
if (typeof widget.callback === 'function') widget.callback(value)
|
||||
if (typeof node?.setDirtyCanvas === 'function') node.setDirtyCanvas(true, true)
|
||||
}
|
||||
|
||||
function hideWidget(widget) {
|
||||
if (!widget || widget.__autoAudioHidden) return
|
||||
widget.__autoAudioHidden = true
|
||||
widget.hidden = true
|
||||
widget.type = 'converted-widget'
|
||||
widget.computeSize = () => [0, 0]
|
||||
widget.draw = () => {}
|
||||
widget.mouse = () => false
|
||||
widget.serializeValue = () => widget.value
|
||||
if (widget.element) {
|
||||
widget.element.style.display = 'none'
|
||||
}
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
name: 'custom.auto_audio_player',
|
||||
|
||||
async beforeRegisterNodeDef(nodeType, nodeData) {
|
||||
const nodeName = nodeData?.name || nodeData?.display_name || nodeType?.comfyClass || nodeType?.type
|
||||
if (nodeName !== 'AutoAudioPlayer') return
|
||||
|
||||
const onNodeCreated = nodeType.prototype.onNodeCreated
|
||||
nodeType.prototype.onNodeCreated = function () {
|
||||
const result = onNodeCreated?.apply(this, arguments)
|
||||
|
||||
if (this.__autoAudioPlayerInitialized) return result
|
||||
this.__autoAudioPlayerInitialized = true
|
||||
|
||||
const autoplayWidget = findWidget(this, 'autoplay')
|
||||
const volumeWidget = findWidget(this, 'default_volume')
|
||||
const loopWidget = findWidget(this, 'loop')
|
||||
hideWidget(autoplayWidget)
|
||||
hideWidget(volumeWidget)
|
||||
hideWidget(loopWidget)
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
hideWidget(autoplayWidget)
|
||||
hideWidget(volumeWidget)
|
||||
hideWidget(loopWidget)
|
||||
this.setDirtyCanvas?.(true, true)
|
||||
})
|
||||
|
||||
const container = document.createElement('div')
|
||||
container.style.display = 'flex'
|
||||
container.style.flexDirection = 'column'
|
||||
container.style.gap = '10px'
|
||||
container.style.padding = '12px'
|
||||
container.style.width = '100%'
|
||||
container.style.boxSizing = 'border-box'
|
||||
container.style.background = 'rgba(255,255,255,0.04)'
|
||||
container.style.border = '1px solid rgba(255,255,255,0.08)'
|
||||
container.style.borderRadius = '12px'
|
||||
container.style.color = '#e8e8ea'
|
||||
container.style.fontSize = '12px'
|
||||
container.style.fontFamily = 'inherit'
|
||||
container.style.overflow = 'hidden'
|
||||
|
||||
const status = document.createElement('div')
|
||||
status.textContent = 'Waiting for audio...'
|
||||
status.style.fontSize = '12px'
|
||||
status.style.lineHeight = '1.35'
|
||||
status.style.fontWeight = '600'
|
||||
status.style.opacity = '0.95'
|
||||
status.style.wordBreak = 'break-word'
|
||||
|
||||
const audio = document.createElement('audio')
|
||||
audio.preload = 'auto'
|
||||
audio.style.display = 'none'
|
||||
|
||||
const playRow = document.createElement('div')
|
||||
playRow.style.display = 'grid'
|
||||
playRow.style.gridTemplateColumns = '64px minmax(0,1fr) auto'
|
||||
playRow.style.alignItems = 'center'
|
||||
playRow.style.columnGap = '10px'
|
||||
playRow.style.rowGap = '6px'
|
||||
|
||||
const playPauseButton = document.createElement('button')
|
||||
playPauseButton.textContent = 'Play'
|
||||
playPauseButton.style.height = '34px'
|
||||
playPauseButton.style.minWidth = '64px'
|
||||
playPauseButton.style.padding = '0 12px'
|
||||
playPauseButton.style.border = '1px solid rgba(255,255,255,0.10)'
|
||||
playPauseButton.style.borderRadius = '10px'
|
||||
playPauseButton.style.background = 'rgba(255,255,255,0.10)'
|
||||
playPauseButton.style.color = '#f2f2f3'
|
||||
playPauseButton.style.cursor = 'pointer'
|
||||
playPauseButton.style.fontWeight = '700'
|
||||
playPauseButton.style.fontSize = '12px'
|
||||
playPauseButton.style.lineHeight = '1'
|
||||
|
||||
const progress = document.createElement('input')
|
||||
progress.type = 'range'
|
||||
progress.min = '0'
|
||||
progress.max = '1000'
|
||||
progress.step = '1'
|
||||
progress.value = '0'
|
||||
styleRange(progress)
|
||||
|
||||
const timeLabel = document.createElement('div')
|
||||
timeLabel.textContent = '0:00 / 0:00'
|
||||
timeLabel.style.fontSize = '12px'
|
||||
timeLabel.style.opacity = '0.92'
|
||||
timeLabel.style.minWidth = '78px'
|
||||
timeLabel.style.textAlign = 'right'
|
||||
timeLabel.style.fontVariantNumeric = 'tabular-nums'
|
||||
timeLabel.style.whiteSpace = 'nowrap'
|
||||
timeLabel.style.fontWeight = '600'
|
||||
|
||||
playRow.append(playPauseButton, progress, timeLabel)
|
||||
|
||||
const controlsGrid = document.createElement('div')
|
||||
controlsGrid.style.display = 'grid'
|
||||
controlsGrid.style.gridTemplateColumns = '64px minmax(0,1fr) auto'
|
||||
controlsGrid.style.alignItems = 'center'
|
||||
controlsGrid.style.columnGap = '10px'
|
||||
controlsGrid.style.rowGap = '10px'
|
||||
|
||||
const volumeLabel = document.createElement('div')
|
||||
volumeLabel.textContent = 'Vol'
|
||||
volumeLabel.style.fontWeight = '700'
|
||||
volumeLabel.style.opacity = '0.95'
|
||||
|
||||
const volume = document.createElement('input')
|
||||
volume.type = 'range'
|
||||
volume.min = '0'
|
||||
volume.max = '1'
|
||||
volume.step = '0.01'
|
||||
volume.value = String(volumeWidget?.value ?? 1)
|
||||
styleRange(volume)
|
||||
|
||||
const loopWrap = document.createElement('label')
|
||||
loopWrap.style.display = 'inline-flex'
|
||||
loopWrap.style.alignItems = 'center'
|
||||
loopWrap.style.justifySelf = 'end'
|
||||
loopWrap.style.gap = '6px'
|
||||
loopWrap.style.whiteSpace = 'nowrap'
|
||||
loopWrap.style.cursor = 'pointer'
|
||||
loopWrap.style.userSelect = 'none'
|
||||
loopWrap.style.fontWeight = '700'
|
||||
|
||||
const loopToggle = document.createElement('input')
|
||||
loopToggle.type = 'checkbox'
|
||||
loopToggle.style.margin = '0'
|
||||
loopToggle.style.accentColor = '#b866ff'
|
||||
loopToggle.checked = !!loopWidget?.value
|
||||
|
||||
const loopText = document.createElement('span')
|
||||
loopText.textContent = 'Loop'
|
||||
|
||||
loopWrap.append(loopToggle, loopText)
|
||||
|
||||
const autoplayLabel = document.createElement('div')
|
||||
autoplayLabel.textContent = 'Autoplay'
|
||||
autoplayLabel.style.fontWeight = '700'
|
||||
autoplayLabel.style.opacity = '0.95'
|
||||
|
||||
const autoplayToggleWrap = document.createElement('label')
|
||||
autoplayToggleWrap.style.display = 'inline-flex'
|
||||
autoplayToggleWrap.style.alignItems = 'center'
|
||||
autoplayToggleWrap.style.justifySelf = 'start'
|
||||
autoplayToggleWrap.style.gap = '6px'
|
||||
autoplayToggleWrap.style.cursor = 'pointer'
|
||||
autoplayToggleWrap.style.userSelect = 'none'
|
||||
autoplayToggleWrap.style.fontWeight = '700'
|
||||
|
||||
const autoplayToggle = document.createElement('input')
|
||||
autoplayToggle.type = 'checkbox'
|
||||
autoplayToggle.style.margin = '0'
|
||||
autoplayToggle.style.accentColor = '#b866ff'
|
||||
autoplayToggle.checked = autoplayWidget?.value !== false
|
||||
|
||||
autoplayToggleWrap.append(autoplayToggle)
|
||||
|
||||
const spacer = document.createElement('div')
|
||||
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)
|
||||
this.size = [Math.max(this.size?.[0] || 320, 360), Math.max(this.size?.[1] || 220, 200)]
|
||||
this.setDirtyCanvas(true, true)
|
||||
|
||||
const syncButtonLabel = () => {
|
||||
playPauseButton.textContent = audio.paused ? 'Play' : 'Pause'
|
||||
}
|
||||
|
||||
const syncTimeLabel = () => {
|
||||
const current = audio.currentTime || 0
|
||||
const total = Number.isFinite(audio.duration) ? audio.duration : 0
|
||||
timeLabel.textContent = `${formatTime(current)} / ${formatTime(total)}`
|
||||
}
|
||||
|
||||
const syncProgress = () => {
|
||||
const total = Number.isFinite(audio.duration) ? audio.duration : 0
|
||||
if (total <= 0) {
|
||||
progress.value = '0'
|
||||
return
|
||||
}
|
||||
progress.value = String(Math.round((audio.currentTime / total) * 1000))
|
||||
}
|
||||
|
||||
playPauseButton.onclick = async () => {
|
||||
if (!audio.src) return
|
||||
try {
|
||||
if (audio.paused) {
|
||||
await audio.play()
|
||||
} else {
|
||||
audio.pause()
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[AutoAudioPlayer] play/pause error:', err)
|
||||
}
|
||||
syncButtonLabel()
|
||||
}
|
||||
|
||||
progress.oninput = () => {
|
||||
const total = Number.isFinite(audio.duration) ? audio.duration : 0
|
||||
if (total <= 0) return
|
||||
audio.currentTime = (Number(progress.value) / 1000) * total
|
||||
syncTimeLabel()
|
||||
}
|
||||
|
||||
volume.oninput = () => {
|
||||
const value = Number(volume.value)
|
||||
audio.volume = value
|
||||
setWidgetValue(volumeWidget, value, this)
|
||||
}
|
||||
|
||||
loopToggle.onchange = () => {
|
||||
audio.loop = loopToggle.checked
|
||||
setWidgetValue(loopWidget, loopToggle.checked, this)
|
||||
}
|
||||
|
||||
autoplayToggle.onchange = () => {
|
||||
setWidgetValue(autoplayWidget, autoplayToggle.checked, this)
|
||||
}
|
||||
|
||||
audio.addEventListener('play', syncButtonLabel)
|
||||
audio.addEventListener('pause', syncButtonLabel)
|
||||
audio.addEventListener('timeupdate', () => {
|
||||
syncProgress()
|
||||
syncTimeLabel()
|
||||
})
|
||||
audio.addEventListener('loadedmetadata', () => {
|
||||
syncProgress()
|
||||
syncTimeLabel()
|
||||
})
|
||||
audio.addEventListener('ended', () => {
|
||||
syncButtonLabel()
|
||||
syncProgress()
|
||||
syncTimeLabel()
|
||||
})
|
||||
|
||||
this.__autoAudioPlayer = {
|
||||
status,
|
||||
audio,
|
||||
progress,
|
||||
volume,
|
||||
loopToggle,
|
||||
autoplayToggle,
|
||||
syncButtonLabel,
|
||||
syncProgress,
|
||||
syncTimeLabel,
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user