From 400d4f6caf8ba63065870c9dccbaca7579f283b3 Mon Sep 17 00:00:00 2001 From: "Salvador E. Tropea" Date: Wed, 16 Jul 2025 09:26:39 -0300 Subject: [PATCH] [Added][Audio Concatenation] With test and example --- README.md | 36 ++++++++--- example_workflows/cut_and_concat.jpg | Bin 0 -> 11668 bytes example_workflows/cut_and_concat.json | 1 + source/nodes/nodes_audio.py | 66 +++++++++++++++++++ source/tests/test_audio_concatenate.py | 86 +++++++++++++++++++++++++ 5 files changed, 179 insertions(+), 10 deletions(-) create mode 100644 example_workflows/cut_and_concat.jpg create mode 100644 example_workflows/cut_and_concat.json create mode 100644 source/tests/test_audio_concatenate.py diff --git a/README.md b/README.md index 38400d4..12ae940 100644 --- a/README.md +++ b/README.md @@ -15,11 +15,12 @@ workflows, especially when dealing with multiple audio inputs or outputs. - [6. Audio Channel Conv and Resampler](#6-audio-channel-conv-and-resampler) - [7. Audio Information](#7-audio-information) - [8. Audio Cut](#8-audio-cut) - - [9. Audio Blend](#9-audio-blend) - - [10. Audio Test Signal Generator](#10-audio-test-signal-generator) - - [11. Audio Musical Note](#11-audio-musical-note) - - [12. Audio Join 2 Channels](#12-audio-join-2-channels) - - [13. Audio Split 2 Channels](#13-audio-split-2-channels) + - [9. Audio Concatenate](#8-audio-concatenate) + - [10. Audio Blend](#9-audio-blend) + - [11. Audio Test Signal Generator](#10-audio-test-signal-generator) + - [12. Audio Musical Note](#11-audio-musical-note) + - [13. Audio Join 2 Channels](#12-audio-join-2-channels) + - [14. Audio Split 2 Channels](#13-audio-split-2-channels) - [🚀 Installation](#-installation) - [📦 Dependencies](#-dependencies) - [🖼️ Examples](#️-examples) @@ -149,7 +150,21 @@ workflows, especially when dealing with multiple audio inputs or outputs. - **Output:** - `audio_out` (AUDIO): The selected portion of the audio. -### 9. Audio Blend +### 9. Audio Concatenate + - **Display Name:** `Audio Concatenate` + - **Internal Name:** `SET_AudioConcatenate` + - **Category:** `audio/manipulation` + - **Description:** Joins two audio clips end-to-end in time. This is the counterpart to the "Audio Cut" node. + - **Inputs:** + - `audio1` (AUDIO): The first audio clip to appear in the sequence. + - `audio2` (AUDIO): The second audio clip to be appended to the end of the first. + - **Output:** + - `audio_out` (AUDIO): A single audio clip containing `audio1` followed immediately by `audio2`. + - **Behavior Details:** + - **Alignment:** Before concatenation, the two audio inputs are aligned to have the same sample rate and channel count, using the same logic as the "Batch Audios" node. + - **Batch Handling:** If the inputs have different batch sizes, the last item of the shorter batch is repeated to ensure the output batch size matches the larger of the two inputs. + +### 10. Audio Blend - **Display Name:** `Audio Blend` - **Internal Name:** `SET_AudioBlend` - **Category:** `audio/manipulation` @@ -162,7 +177,7 @@ workflows, especially when dealing with multiple audio inputs or outputs. - **Output:** - `audio_out` (AUDIO): The blended audio. -### 10. Audio Test Signal Generator +### 11. Audio Test Signal Generator - **Display Name:** `Audio Test Signal Generator` - **Internal Name:** `SET_AudioTestSignalGenerator` - **Category:** `audio/generation` @@ -186,7 +201,7 @@ workflows, especially when dealing with multiple audio inputs or outputs. - **Output:** - `audio_out` (AUDIO): The generated test signal. -### 11. Audio Musical Note +### 12. Audio Musical Note - **Display Name:** `Audio Musical Note` - **Internal Name:** `SET_AudioMusicalNote` - **Category:** `audio/generation` @@ -197,7 +212,7 @@ workflows, especially when dealing with multiple audio inputs or outputs. - **Output:** - `frequency` (FLOAT): The calculated frequency of the note in Hz. -### 12. Audio Join 2 Channels +### 13. Audio Join 2 Channels - **Display Name:** `Audio Join 2 Channels` - **Internal Name:** `SET_AudioJoin2Channels` - **Category:** `audio/manipulation` @@ -212,7 +227,7 @@ workflows, especially when dealing with multiple audio inputs or outputs. - **Alignment:** The two mono signals are then aligned to have the same sample rate and length, using the same logic as the "Batch Audios" node (resamples to match `audio_left`'s SR, pads to match the longest duration). - **Batch Handling:** If the inputs have different batch sizes, the last item of the shorter batch is repeated to match the length of the longer batch. -### 13. Audio Split 2 Channels +### 14. Audio Split 2 Channels - **Display Name:** `Audio Split 2 Channels` - **Internal Name:** `SET_AudioSplit2Channels` - **Category:** `audio/manipulation` @@ -254,6 +269,7 @@ Once installed the examples are available in the ComfyUI workflow templates, in and the sample rate. - [generate_and_blend.json](example_workflows/generate_and_blend.json): Shows how to generate four musical notes and blend them together to create a chord. +- [cut_and_concat.json](example_workflows/cut_and_concat.json): Shows how to cut and concatenate audio. ## 📝 Usage Notes diff --git a/example_workflows/cut_and_concat.jpg b/example_workflows/cut_and_concat.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f2d843a0426b6947de0935295a59d345a7b35887 GIT binary patch literal 11668 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 rlX(rh2+#