[Added][Audio Musical Note] With test and example

This commit is contained in:
Salvador E. Tropea
2025-07-16 08:16:51 -03:00
parent 0915bd1492
commit c5e110e58b
7 changed files with 199 additions and 3 deletions
+14
View File
@@ -17,6 +17,7 @@ workflows, especially when dealing with multiple audio inputs or outputs.
- [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)
- [🚀 Installation](#-installation)
- [📦 Dependencies](#-dependencies)
- [🖼️ Examples](#️-examples)
@@ -183,6 +184,17 @@ workflows, especially when dealing with multiple audio inputs or outputs.
- **Output:**
- `audio_out` (AUDIO): The generated test signal.
### 11. Audio Musical Note
- **Display Name:** `Audio Musical Note`
- **Internal Name:** `SET_AudioMusicalNote`
- **Category:** `audio/generation`
- **Description:** Converts a musical note (e.g., C#, Gb) and an octave into its corresponding frequency in Hz. This is perfect for accurately setting the frequency of the `Audio Test Signal Generator` node.
- **Inputs:**
- `note` (STRING): The musical note name. It's case-insensitive and flexible, accepting formats like `"C#"`, `"Db"`, `"g sharp"`, or `"a flat"`.
- `octave` (INT): The octave number for the note. Octave 4 is the standard middle range (containing A4=440Hz).
- **Output:**
- `frequency` (FLOAT): The calculated frequency of the note in Hz.
## 🚀 Installation
You can install the nodes from the ComfyUI nodes manager, the name is *Audio Batch*, or just do it manually:
@@ -212,6 +224,8 @@ Once installed the examples are available in the ComfyUI workflow templates, in
how to extract a single element from the batch.
- [resample_force_stereo.json](example_workflows/resample_force_stereo.json): Shows how to change the number of channels
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.
## 📝 Usage Notes
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

File diff suppressed because one or more lines are too long
+41 -1
View File
@@ -9,7 +9,7 @@ import torchaudio
import torchaudio.transforms as T
from .utils.aligner import AudioBatchAligner
from .utils.logger import main_logger
from .utils.misc import parse_time_to_seconds
from .utils.misc import parse_time_to_seconds, parse_note_to_frequency
logger = main_logger
BASE_CATEGORY = "audio"
@@ -619,3 +619,43 @@ class AudioTestSignalGenerator:
}
return (output_audio,)
class AudioMusicalNote:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"note": ("STRING", {
"default": "A",
"tooltip": "The musical note in American notation (e.g., C, F#, Gb, 'D sharp'). Case-insensitive."
}),
"octave": ("INT", {
"default": 4,
"min": 0,
"max": 8, # Standard piano range is roughly 0-8
"step": 1,
"tooltip": "The octave number (e.g., 4 corresponds to middle C's octave)."
}),
}
}
RETURN_TYPES = ("FLOAT",)
RETURN_NAMES = ("frequency",)
FUNCTION = "get_frequency"
CATEGORY = BASE_CATEGORY + "/" + GEN_CATEGORY
DESCRIPTION = "Converts a musical note and octave to its corresponding frequency in Hz."
UNIQUE_NAME = "SET_AudioMusicalNote"
DISPLAY_NAME = "Audio Musical Note"
def get_frequency(self, note: str, octave: int):
try:
frequency = parse_note_to_frequency(note, octave)
logger.info(f"Parsed note '{note}{octave}' to {frequency:.2f} Hz.")
return (frequency,)
except (ValueError, TypeError) as e:
# If the user enters an invalid note, log it as a warning
# and return a default safe frequency (e.g., A4) to avoid crashing.
# A UI warning could also be sent if this were a generator.
logger.error(f"Error parsing note: {e}. Defaulting to 440.0 Hz.")
return (440.0,)
+57
View File
@@ -2,6 +2,7 @@
# Copyright (c) 2025 Instituto Nacional de Tecnologïa Industrial
# License: GPL-3.0
# Project: ComfyUI-AudioBatch
import re
NODES_NAME = "AudioBatch"
NODES_DEBUG_VAR = NODES_NAME.upper() + "_NODES_DEBUG"
@@ -65,3 +66,59 @@ def parse_time_to_seconds(time_str: str) -> float:
"Expected 'SECONDS', 'MM:SS.ss', or 'HH:MM:SS.ss'.") from e
except RuntimeError as e:
raise ValueError(str(e))
# Reference frequency for A4, the standard tuning pitch
A4_FREQ = 440.0
NOTES = {
'c': -9, 'c#': -8, 'db': -8,
'd': -7, 'd#': -6, 'eb': -6,
'e': -5,
'f': -4, 'f#': -3, 'gb': -3,
'g': -2, 'g#': -1, 'ab': -1,
'a': 0, 'a#': 1, 'bb': 1,
'b': 2,
}
def parse_note_to_frequency(note_str: str, octave: int) -> float:
"""
Parses a musical note string (e.g., `C#`, `A flat`, `db`) and an octave
to calculate its frequency in Hz.
Args:
note_str (str): The note name. Case-insensitive. Handles sharps ( #), flats (b),
and text (`sharp`, `flat`).
octave (int): The octave number (e.g., 4 for middle C's octave).
Returns:
float: The frequency of the note in Hz.
Raises:
ValueError: If the note name is invalid.
"""
if not isinstance(note_str, str):
raise TypeError("Note name must be a string.")
# Normalize the string: lowercase, remove "sharp" or "flat" text, remove spaces
processed_str = note_str.lower().strip()
processed_str = re.sub(r'\s*sharp\s*', '#', processed_str)
processed_str = re.sub(r'\s*flat\s*', 'b', processed_str)
processed_str = processed_str.replace(" ", "")
if processed_str not in NOTES:
raise ValueError(f"Invalid note name: '{note_str}'. Could not parse to a valid note.")
# Get the number of semitones away from A
semitone_offset_from_a = NOTES[processed_str]
# Calculate the number of semitones away from A4 (A in the 4th octave)
# The note "A" in octave 4 is our base (0 semitones from itself).
# The note "A" in octave 5 is 12 semitones higher.
# The note "C" in octave 4 is -9 semitones from A4.
n = semitone_offset_from_a + (octave - 4) * 12
# Apply the frequency formula: f = f_base * (2^(1/12))^n
frequency = A4_FREQ * (2**(1/12))**n
return frequency
+37
View File
@@ -0,0 +1,37 @@
"""
Regression tests for the AudioMusicalNote node in ComfyUI-AudioBatch.
"""
import bootstrap # noqa: F401
import pytest
from nodes.nodes_audio import AudioMusicalNote
@pytest.fixture
def note_node():
return AudioMusicalNote()
def test_note_node_valid_input(note_node):
"""Tests the node's integration with the parser for a valid note."""
note = "C"
octave = 4
(frequency,) = note_node.get_frequency(note, octave)
assert frequency == pytest.approx(261.63, rel=0.001)
def test_note_node_invalid_input(note_node):
"""
Tests the node's error handling for an invalid note.
It should not crash and should return a default frequency.
"""
note = "Z" # Invalid note
octave = 4
# The node should catch the ValueError from the parser and return a default
(frequency,) = note_node.get_frequency(note, octave)
# Assert that it returned the default fallback frequency
assert frequency == 440.0
+49 -2
View File
@@ -4,8 +4,8 @@ Tests for miscellaneous utility functions found in utils/misc.py.
import bootstrap # noqa: F401
import pytest
# Import the function to be tested
from nodes.utils.misc import parse_time_to_seconds
# Import the functions to be tested
from nodes.utils.misc import parse_note_to_frequency, parse_time_to_seconds
# --- Test Cases for parse_time_to_seconds ---
@@ -87,3 +87,50 @@ def test_parse_time_non_string_input_raises_error():
with pytest.raises(TypeError):
parse_time_to_seconds({"time": "10"}) # Pass a dict
# --- Test Cases for parse_note_to_frequency ---
@pytest.mark.parametrize("note, octave, expected_freq", [
# Reference notes
("A", 4, 440.0), # A4 standard pitch
("C", 4, 261.63), # Middle C
("C", 0, 16.35), # Very low C
# Different notations
("C#", 4, 277.18), # C sharp
("c sharp", 4, 277.18), # C sharp with text
("c sharp", 4, 277.18), # C sharp with extra space
("Db", 4, 277.18), # D flat (same as C#)
("d flat", 4, 277.18), # D flat with text
("d b", 4, 277.18), # D flat with extra space
# Different octaves
("A", 5, 880.0), # One octave higher
("A", 3, 220.0), # One octave lower
])
def test_parse_note_valid_inputs(note, octave, expected_freq):
"""Tests that valid note strings are parsed to the correct frequencies."""
assert parse_note_to_frequency(note, octave) == pytest.approx(expected_freq, rel=0.001)
@pytest.mark.parametrize("invalid_note_str", [
"H", # Not a valid note letter
"C##", # Double sharp not supported by this simple parser
"Dbb", # Double flat not supported
"A Sharps", # Invalid text
"123", # Not a note
"", # Empty string
])
def test_parse_note_invalid_notes_raise_error(invalid_note_str):
"""Tests that invalid note names raise ValueError."""
with pytest.raises(ValueError, match="Invalid note name"):
parse_note_to_frequency(invalid_note_str, 4)
def test_parse_note_invalid_type_raises_error():
"""Tests that non-string inputs raise TypeError."""
with pytest.raises(TypeError):
parse_note_to_frequency(123, 4)
with pytest.raises(TypeError):
parse_note_to_frequency(None, 4)