Add PlaySoundKJ

This commit is contained in:
kijai
2026-03-30 17:12:39 +03:00
parent d30ac13725
commit fd8a6d2cfe
3 changed files with 135 additions and 0 deletions
+1
View File
@@ -224,6 +224,7 @@ NODE_CONFIG = {
"ModelMemoryUsageFactorOverride": {"class": ModelMemoryUsageFactorOverride, "name": "Model Memory Usage Factor Override"},
"WanChunkFeedForward": {"class": WanChunkFeedForward, "name": "Wan ChunkFeedForward"},
"SamplerSelfRefineVideo": {"class": SamplerSelfRefineVideo, "name": "Sampler SelfRefineVideo"},
"PlaySoundKJ": {"class": PlaySoundKJ, "name": "Play Sound KJ"},
#instance diffusion
"CreateInstanceDiffusionTracking": {"class": CreateInstanceDiffusionTracking},
+40
View File
@@ -3310,3 +3310,43 @@ class PreviewLatentNoiseMask(io.ComfyNode):
noise_mask = noise_mask[0, 0]
return io.NodeOutput(noise_mask)
class PlaySoundKJ(io.ComfyNode):
"""Plays audio in the browser when execution reaches this node."""
@classmethod
def define_schema(cls):
return io.Schema(
node_id="PlaySoundKJ",
category="KJNodes/audio",
description="Plays the input audio in the browser. Modes: 'always' plays on every execution, 'on_empty_queue' plays only when the queue finishes, 'on_change' plays only when the audio content changes. Duration limits playback length (0 = full audio).",
inputs=[
io.Audio.Input("audio"),
io.Combo.Input("mode", options=["always", "on_empty_queue", "on_change"], default="always"),
io.Float.Input("volume", default=0.5, min=0.0, max=1.0, step=0.01),
io.Float.Input("duration", default=5.0, min=0.0, max=300.0, step=0.1, tooltip="Duration in seconds to play. 0 = play full audio."),
io.AnyType.Input("any_input", optional=True),
],
outputs=[
io.AnyType.Output("any_output", display_name="any_output"),
],
is_output_node=True,
)
@classmethod
def fingerprint_inputs(cls, **kwargs):
if kwargs.get("mode") == "on_change":
return False
return float("NaN")
@classmethod
def execute(cls, audio, mode="always", volume=0.5, duration=5.0, any_input=None) -> io.NodeOutput:
preview = ui.PreviewAudio(audio, cls=cls)
ui_dict = preview.as_dict()
waveform = audio["waveform"]
ui_dict["audio_hash"] = [hash(waveform.sum().item())]
return io.NodeOutput(
any_input,
ui=ui_dict,
)
+94
View File
@@ -0,0 +1,94 @@
const { app } = window.comfyAPI.app;
const { api } = window.comfyAPI.api;
app.registerExtension({
name: "KJNodes.PlaySound",
async beforeRegisterNodeDef(nodeType, nodeData) {
if (nodeData.name !== "PlaySoundKJ") return;
const onExecuted = nodeType.prototype.onExecuted;
nodeType.prototype.onExecuted = function (output) {
onExecuted?.call(this, output);
const audios = output?.audio;
if (!audios?.length) return;
const modeWidget = this.widgets?.find(w => w.name === "mode");
const volumeWidget = this.widgets?.find(w => w.name === "volume");
const durationWidget = this.widgets?.find(w => w.name === "duration");
const mode = modeWidget?.value ?? "always";
const volume = volumeWidget?.value ?? 0.5;
const duration = durationWidget?.value ?? 0;
// on_change: skip if audio content hasn't changed
if (mode === "on_change") {
const audioHash = output?.audio_hash?.[0];
if (audioHash != null && this._kjLastAudioHash === audioHash) return;
this._kjLastAudioHash = audioHash;
}
// Clean up previous state
if (this._kjStatusListener) {
api.removeEventListener("status", this._kjStatusListener);
this._kjStatusListener = null;
}
clearTimeout(this._kjQueueDebounce);
this._kjPendingAudio = null;
if (this._kjPlayingAudio) {
this._kjPlayingAudio.pause();
this._kjPlayingAudio = null;
}
if (this._kjPlayTimer != null) {
clearTimeout(this._kjPlayTimer);
this._kjPlayTimer = null;
}
const startPlayback = () => {
const { filename, subfolder, type } = audios[0];
const params = new URLSearchParams({
filename: filename ?? "",
subfolder: subfolder ?? "",
type: type ?? "temp",
});
const url = api.apiURL(`/view?${params.toString()}`);
const audio = new Audio(url);
audio.volume = Math.max(0, Math.min(1, volume));
audio.play().catch(() => {});
this._kjPlayingAudio = audio;
if (duration > 0) {
this._kjPlayTimer = setTimeout(() => {
audio.pause();
this._kjPlayingAudio = null;
this._kjPlayTimer = null;
}, duration * 1000);
}
};
if (mode === "on_empty_queue") {
this._kjPendingAudio = startPlayback;
this._kjStatusListener = ({ detail }) => {
const remaining = detail?.exec_info?.queue_remaining ?? 0;
if (remaining === 0) {
// Debounce: confirm queue is truly empty
// (status can briefly show 0 between dispatches)
clearTimeout(this._kjQueueDebounce);
this._kjQueueDebounce = setTimeout(() => {
if (this._kjPendingAudio) {
this._kjPendingAudio();
this._kjPendingAudio = null;
}
api.removeEventListener("status", this._kjStatusListener);
this._kjStatusListener = null;
}, 1000);
} else {
clearTimeout(this._kjQueueDebounce);
}
};
api.addEventListener("status", this._kjStatusListener);
} else {
startPlayback();
}
};
},
});