Add PlaySoundKJ
This commit is contained in:
@@ -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},
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user