diff --git a/README.md b/README.md index 5dd6990..0099c69 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/example_workflows/generate_and_blend.jpg b/example_workflows/generate_and_blend.jpg new file mode 100644 index 0000000..6db97f3 Binary files /dev/null and b/example_workflows/generate_and_blend.jpg differ diff --git a/example_workflows/generate_and_blend.json b/example_workflows/generate_and_blend.json new file mode 100644 index 0000000..72875e8 --- /dev/null +++ b/example_workflows/generate_and_blend.json @@ -0,0 +1 @@ +{"id":"ddd6dc77-8ca6-4092-879a-0e1d0bb30c0f","revision":0,"last_node_id":20,"last_link_id":24,"nodes":[{"id":6,"type":"SET_AudioMusicalNote","pos":[814.1455688476562,1181.8890380859375],"size":[270,82],"flags":{},"order":0,"mode":0,"inputs":[{"localized_name":"note","name":"note","type":"STRING","widget":{"name":"note"},"link":null},{"localized_name":"octave","name":"octave","type":"INT","widget":{"name":"octave"},"link":null}],"outputs":[{"localized_name":"frequency","name":"frequency","type":"FLOAT","links":[2]}],"properties":{"aux_id":"set-soft/ComfyUI-AudioBatch","ver":"0915bd14926f6d968a46bb28179be8feea86a270","Node name for S&R":"SET_AudioMusicalNote"},"widgets_values":["C",4],"color":"#223","bgcolor":"#335"},{"id":10,"type":"SET_AudioMusicalNote","pos":[814.1455688476562,1953.298095703125],"size":[270,82],"flags":{},"order":1,"mode":0,"inputs":[{"localized_name":"note","name":"note","type":"STRING","widget":{"name":"note"},"link":null},{"localized_name":"octave","name":"octave","type":"INT","widget":{"name":"octave"},"link":null}],"outputs":[{"localized_name":"frequency","name":"frequency","type":"FLOAT","links":[7]}],"properties":{"aux_id":"set-soft/ComfyUI-AudioBatch","ver":"0915bd14926f6d968a46bb28179be8feea86a270","Node name for S&R":"SET_AudioMusicalNote"},"widgets_values":["G",4],"color":"#223","bgcolor":"#335"},{"id":12,"type":"SET_AudioMusicalNote","pos":[814.1455688476562,2332.916748046875],"size":[270,82],"flags":{},"order":2,"mode":0,"inputs":[{"localized_name":"note","name":"note","type":"STRING","widget":{"name":"note"},"link":null},{"localized_name":"octave","name":"octave","type":"INT","widget":{"name":"octave"},"link":null}],"outputs":[{"localized_name":"frequency","name":"frequency","type":"FLOAT","links":[10]}],"properties":{"aux_id":"set-soft/ComfyUI-AudioBatch","ver":"0915bd14926f6d968a46bb28179be8feea86a270","Node name for S&R":"SET_AudioMusicalNote"},"widgets_values":["B",4],"color":"#223","bgcolor":"#335"},{"id":4,"type":"SET_AudioTestSignalGenerator","pos":[1207.3983154296875,1137.322021484375],"size":[275.0025329589844,322],"flags":{},"order":8,"mode":0,"inputs":[{"localized_name":"waveform_type","name":"waveform_type","type":"COMBO","widget":{"name":"waveform_type"},"link":null},{"localized_name":"frequency","name":"frequency","type":"FLOAT","widget":{"name":"frequency"},"link":2},{"localized_name":"frequency_end","name":"frequency_end","type":"FLOAT","widget":{"name":"frequency_end"},"link":null},{"localized_name":"amplitude","name":"amplitude","type":"FLOAT","widget":{"name":"amplitude"},"link":null},{"localized_name":"dc_offset","name":"dc_offset","type":"FLOAT","widget":{"name":"dc_offset"},"link":null},{"localized_name":"phase","name":"phase","type":"FLOAT","widget":{"name":"phase"},"link":null},{"localized_name":"duration","name":"duration","type":"STRING","widget":{"name":"duration"},"link":4},{"localized_name":"sample_rate","name":"sample_rate","type":"INT","widget":{"name":"sample_rate"},"link":null},{"localized_name":"batch_size","name":"batch_size","type":"INT","widget":{"name":"batch_size"},"link":null},{"localized_name":"channels","name":"channels","type":"INT","widget":{"name":"channels"},"link":null},{"localized_name":"seed","name":"seed","shape":7,"type":"INT","widget":{"name":"seed"},"link":null}],"outputs":[{"localized_name":"audio_out","name":"audio_out","type":"AUDIO","links":[13]}],"properties":{"aux_id":"set-soft/ComfyUI-AudioBatch","ver":"7388ff7a1db5bb56ef9add8c6520fc9affd61d8c","Node name for S&R":"SET_AudioTestSignalGenerator"},"widgets_values":["sine",440,10000,1,0,0,"10",44100,1,1,574646830867686,"randomize"],"color":"#232","bgcolor":"#353"},{"id":13,"type":"SET_AudioTestSignalGenerator","pos":[1207.3983154296875,2288.349853515625],"size":[275.0025329589844,322],"flags":{},"order":11,"mode":0,"inputs":[{"localized_name":"waveform_type","name":"waveform_type","type":"COMBO","widget":{"name":"waveform_type"},"link":null},{"localized_name":"frequency","name":"frequency","type":"FLOAT","widget":{"name":"frequency"},"link":10},{"localized_name":"frequency_end","name":"frequency_end","type":"FLOAT","widget":{"name":"frequency_end"},"link":null},{"localized_name":"amplitude","name":"amplitude","type":"FLOAT","widget":{"name":"amplitude"},"link":null},{"localized_name":"dc_offset","name":"dc_offset","type":"FLOAT","widget":{"name":"dc_offset"},"link":null},{"localized_name":"phase","name":"phase","type":"FLOAT","widget":{"name":"phase"},"link":null},{"localized_name":"duration","name":"duration","type":"STRING","widget":{"name":"duration"},"link":24},{"localized_name":"sample_rate","name":"sample_rate","type":"INT","widget":{"name":"sample_rate"},"link":null},{"localized_name":"batch_size","name":"batch_size","type":"INT","widget":{"name":"batch_size"},"link":null},{"localized_name":"channels","name":"channels","type":"INT","widget":{"name":"channels"},"link":null},{"localized_name":"seed","name":"seed","shape":7,"type":"INT","widget":{"name":"seed"},"link":null}],"outputs":[{"localized_name":"audio_out","name":"audio_out","type":"AUDIO","links":[17]}],"properties":{"aux_id":"set-soft/ComfyUI-AudioBatch","ver":"7388ff7a1db5bb56ef9add8c6520fc9affd61d8c","Node name for S&R":"SET_AudioTestSignalGenerator"},"widgets_values":["sine",440,10000,1,0,0,"10",44100,1,1,1106837879687244,"randomize"],"color":"#232","bgcolor":"#353"},{"id":11,"type":"SET_AudioTestSignalGenerator","pos":[1207.3983154296875,1908.7310791015625],"size":[275.0025329589844,322],"flags":{},"order":10,"mode":0,"inputs":[{"localized_name":"waveform_type","name":"waveform_type","type":"COMBO","widget":{"name":"waveform_type"},"link":null},{"localized_name":"frequency","name":"frequency","type":"FLOAT","widget":{"name":"frequency"},"link":7},{"localized_name":"frequency_end","name":"frequency_end","type":"FLOAT","widget":{"name":"frequency_end"},"link":null},{"localized_name":"amplitude","name":"amplitude","type":"FLOAT","widget":{"name":"amplitude"},"link":null},{"localized_name":"dc_offset","name":"dc_offset","type":"FLOAT","widget":{"name":"dc_offset"},"link":null},{"localized_name":"phase","name":"phase","type":"FLOAT","widget":{"name":"phase"},"link":null},{"localized_name":"duration","name":"duration","type":"STRING","widget":{"name":"duration"},"link":23},{"localized_name":"sample_rate","name":"sample_rate","type":"INT","widget":{"name":"sample_rate"},"link":null},{"localized_name":"batch_size","name":"batch_size","type":"INT","widget":{"name":"batch_size"},"link":null},{"localized_name":"channels","name":"channels","type":"INT","widget":{"name":"channels"},"link":null},{"localized_name":"seed","name":"seed","shape":7,"type":"INT","widget":{"name":"seed"},"link":null}],"outputs":[{"localized_name":"audio_out","name":"audio_out","type":"AUDIO","links":[18]}],"properties":{"aux_id":"set-soft/ComfyUI-AudioBatch","ver":"7388ff7a1db5bb56ef9add8c6520fc9affd61d8c","Node name for S&R":"SET_AudioTestSignalGenerator"},"widgets_values":["sine",440,10000,1,0,0,"10",44100,1,1,239814113097542,"randomize"],"color":"#232","bgcolor":"#353"},{"id":16,"type":"SET_AudioBlend","pos":[1654.5496826171875,2105.652099609375],"size":[270,102],"flags":{},"order":13,"mode":0,"inputs":[{"localized_name":"audio1","name":"audio1","type":"AUDIO","link":18},{"localized_name":"audio2","name":"audio2","shape":7,"type":"AUDIO","link":17},{"localized_name":"gain1","name":"gain1","type":"FLOAT","widget":{"name":"gain1"},"link":null},{"localized_name":"gain2","name":"gain2","type":"FLOAT","widget":{"name":"gain2"},"link":null}],"outputs":[{"localized_name":"audio_out","name":"audio_out","type":"AUDIO","links":[20]}],"properties":{"aux_id":"set-soft/ComfyUI-AudioBatch","ver":"0915bd14926f6d968a46bb28179be8feea86a270","Node name for S&R":"SET_AudioBlend"},"widgets_values":[0.5000000000000001,0.5000000000000001],"color":"#323","bgcolor":"#535"},{"id":14,"type":"SET_AudioBlend","pos":[1654.5496826171875,1305.6060791015625],"size":[270,102],"flags":{},"order":12,"mode":0,"inputs":[{"localized_name":"audio1","name":"audio1","type":"AUDIO","link":13},{"localized_name":"audio2","name":"audio2","shape":7,"type":"AUDIO","link":14},{"localized_name":"gain1","name":"gain1","type":"FLOAT","widget":{"name":"gain1"},"link":null},{"localized_name":"gain2","name":"gain2","type":"FLOAT","widget":{"name":"gain2"},"link":null}],"outputs":[{"localized_name":"audio_out","name":"audio_out","type":"AUDIO","links":[19]}],"properties":{"aux_id":"set-soft/ComfyUI-AudioBatch","ver":"0915bd14926f6d968a46bb28179be8feea86a270","Node name for S&R":"SET_AudioBlend"},"widgets_values":[0.5000000000000001,0.5000000000000001],"color":"#323","bgcolor":"#535"},{"id":8,"type":"SET_AudioTestSignalGenerator","pos":[1207.3983154296875,1521.0072021484375],"size":[275.0025329589844,322],"flags":{},"order":9,"mode":0,"inputs":[{"localized_name":"waveform_type","name":"waveform_type","type":"COMBO","widget":{"name":"waveform_type"},"link":null},{"localized_name":"frequency","name":"frequency","type":"FLOAT","widget":{"name":"frequency"},"link":3},{"localized_name":"frequency_end","name":"frequency_end","type":"FLOAT","widget":{"name":"frequency_end"},"link":null},{"localized_name":"amplitude","name":"amplitude","type":"FLOAT","widget":{"name":"amplitude"},"link":null},{"localized_name":"dc_offset","name":"dc_offset","type":"FLOAT","widget":{"name":"dc_offset"},"link":null},{"localized_name":"phase","name":"phase","type":"FLOAT","widget":{"name":"phase"},"link":null},{"localized_name":"duration","name":"duration","type":"STRING","widget":{"name":"duration"},"link":22},{"localized_name":"sample_rate","name":"sample_rate","type":"INT","widget":{"name":"sample_rate"},"link":null},{"localized_name":"batch_size","name":"batch_size","type":"INT","widget":{"name":"batch_size"},"link":null},{"localized_name":"channels","name":"channels","type":"INT","widget":{"name":"channels"},"link":null},{"localized_name":"seed","name":"seed","shape":7,"type":"INT","widget":{"name":"seed"},"link":null}],"outputs":[{"localized_name":"audio_out","name":"audio_out","type":"AUDIO","links":[14]}],"properties":{"aux_id":"set-soft/ComfyUI-AudioBatch","ver":"7388ff7a1db5bb56ef9add8c6520fc9affd61d8c","Node name for S&R":"SET_AudioTestSignalGenerator"},"widgets_values":["sine",440,10000,1,0,0,"10",44100,1,1,763399735138665,"randomize"],"color":"#232","bgcolor":"#353"},{"id":7,"type":"SET_AudioMusicalNote","pos":[814.1455688476562,1571.3138427734375],"size":[270,82],"flags":{"collapsed":false},"order":3,"mode":0,"inputs":[{"localized_name":"note","name":"note","type":"STRING","widget":{"name":"note"},"link":null},{"localized_name":"octave","name":"octave","type":"INT","widget":{"name":"octave"},"link":null}],"outputs":[{"localized_name":"frequency","name":"frequency","type":"FLOAT","links":[3]}],"properties":{"aux_id":"set-soft/ComfyUI-AudioBatch","ver":"0915bd14926f6d968a46bb28179be8feea86a270","Node name for S&R":"SET_AudioMusicalNote"},"widgets_values":["E",4],"color":"#223","bgcolor":"#335"},{"id":17,"type":"SET_AudioBlend","pos":[2052.178466796875,1677.12158203125],"size":[270,102],"flags":{},"order":14,"mode":0,"inputs":[{"localized_name":"audio1","name":"audio1","type":"AUDIO","link":19},{"localized_name":"audio2","name":"audio2","shape":7,"type":"AUDIO","link":20},{"localized_name":"gain1","name":"gain1","type":"FLOAT","widget":{"name":"gain1"},"link":null},{"localized_name":"gain2","name":"gain2","type":"FLOAT","widget":{"name":"gain2"},"link":null}],"outputs":[{"localized_name":"audio_out","name":"audio_out","type":"AUDIO","links":[21]}],"properties":{"aux_id":"set-soft/ComfyUI-AudioBatch","ver":"0915bd14926f6d968a46bb28179be8feea86a270","Node name for S&R":"SET_AudioBlend"},"widgets_values":[0.5000000000000001,0.5000000000000001],"color":"#323","bgcolor":"#535"},{"id":9,"type":"PrimitiveString","pos":[279.8495178222656,1372.6357421875],"size":[270,58],"flags":{},"order":4,"mode":0,"inputs":[{"localized_name":"value","name":"value","type":"STRING","widget":{"name":"value"},"link":null}],"outputs":[{"localized_name":"STRING","name":"STRING","type":"STRING","links":[4,22,23,24]}],"title":"Duration","properties":{"cnr_id":"comfy-core","ver":"0.3.43","Node name for S&R":"PrimitiveString"},"widgets_values":["3"],"color":"#2a363b","bgcolor":"#3f5159"},{"id":5,"type":"PreviewAudio","pos":[2391.737060546875,1676.059814453125],"size":[270,88],"flags":{},"order":15,"mode":0,"inputs":[{"localized_name":"audio","name":"audio","type":"AUDIO","link":21},{"localized_name":"audioUI","name":"audioUI","type":"AUDIO_UI","widget":{"name":"audioUI"},"link":null}],"outputs":[],"properties":{"cnr_id":"comfy-core","ver":"0.3.43","Node name for S&R":"PreviewAudio"},"widgets_values":[],"color":"#222","bgcolor":"#000"},{"id":18,"type":"MarkdownNote","pos":[282.4097595214844,1231.078125],"size":[260.6913757324219,98.07042694091797],"flags":{},"order":5,"mode":0,"inputs":[],"outputs":[],"properties":{},"widgets_values":["# Duration for the sound"],"color":"#432","bgcolor":"#653"},{"id":19,"type":"MarkdownNote","pos":[819.7695922851562,1014.8834838867188],"size":[259.8813781738281,107.79053497314453],"flags":{},"order":6,"mode":0,"inputs":[],"outputs":[],"properties":{},"widgets_values":["# Musical notes, this is *C Major 7th chord* (C4, E4, G4, B4)"],"color":"#432","bgcolor":"#653"},{"id":20,"type":"MarkdownNote","pos":[1213.787841796875,965.1417236328125],"size":[260.6913757324219,98.07042694091797],"flags":{},"order":7,"mode":0,"inputs":[],"outputs":[],"properties":{},"widgets_values":["# Here we generate the tones"],"color":"#432","bgcolor":"#653"}],"links":[[2,6,0,4,1,"FLOAT"],[3,7,0,8,1,"FLOAT"],[4,9,0,4,6,"STRING"],[7,10,0,11,1,"FLOAT"],[10,12,0,13,1,"FLOAT"],[13,4,0,14,0,"AUDIO"],[14,8,0,14,1,"AUDIO"],[17,13,0,16,1,"AUDIO"],[18,11,0,16,0,"AUDIO"],[19,14,0,17,0,"AUDIO"],[20,16,0,17,1,"AUDIO"],[21,17,0,5,0,"AUDIO"],[22,9,0,8,6,"STRING"],[23,9,0,11,6,"STRING"],[24,9,0,13,6,"STRING"]],"groups":[{"id":1,"title":"Blend the tones","bounding":[1644.5496826171875,1232.006103515625,687.6287841796875,985.64599609375],"color":"#3f789e","font_size":24,"flags":{}}],"config":{},"extra":{"ds":{"scale":1.020292482666827,"offset":[-1144.7691864505493,-1158.794069913624]},"linkExtensions":[{"id":4,"parentId":1},{"id":22,"parentId":2},{"id":23,"parentId":3},{"id":24,"parentId":4}],"reroutes":[{"id":1,"pos":[711.231201171875,1391.44140625],"linkIds":[4,22,23,24]},{"id":2,"parentId":1,"pos":[736.3049926757812,1678.44921875],"linkIds":[22,23,24]},{"id":3,"parentId":2,"pos":[740.4769897460938,2084.7578125],"linkIds":[23,24]},{"id":4,"parentId":3,"pos":[742.891845703125,2464.902587890625],"linkIds":[24]}]},"version":0.4} \ No newline at end of file diff --git a/source/nodes/nodes_audio.py b/source/nodes/nodes_audio.py index 6fddbec..f273ee6 100644 --- a/source/nodes/nodes_audio.py +++ b/source/nodes/nodes_audio.py @@ -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,) diff --git a/source/nodes/utils/misc.py b/source/nodes/utils/misc.py index 096c5e7..ce39d9f 100644 --- a/source/nodes/utils/misc.py +++ b/source/nodes/utils/misc.py @@ -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 diff --git a/source/tests/test_audio_note_node.py b/source/tests/test_audio_note_node.py new file mode 100644 index 0000000..3048d21 --- /dev/null +++ b/source/tests/test_audio_note_node.py @@ -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 diff --git a/source/tests/test_misc_utils.py b/source/tests/test_misc_utils.py index b2b496f..3928a49 100644 --- a/source/tests/test_misc_utils.py +++ b/source/tests/test_misc_utils.py @@ -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)