Spaces:
Running
Running
| """ | |
| MIDI to Frequency Node - Converts a standard MIDI note number and velocity | |
| into a usable frequency (Hz) and amplitude signal. | |
| Place this file in the 'nodes' folder | |
| """ | |
| import numpy as np | |
| from PyQt6 import QtGui | |
| import cv2 | |
| import math | |
| import __main__ | |
| BaseNode = __main__.BaseNode | |
| PA_INSTANCE = getattr(__main__, "PA_INSTANCE", None) | |
| QtGui = __main__.QtGui | |
| # Reference Frequency: A4 = 440 Hz (MIDI note 69) | |
| A4_FREQ = 440.0 | |
| A4_MIDI = 69 | |
| # MIDI Note formula: f = 440 * 2^((N - 69)/12) | |
| class MidiToFreqNode(BaseNode): | |
| NODE_CATEGORY = "Transform" | |
| NODE_COLOR = QtGui.QColor(150, 50, 200) # Musical Purple | |
| def __init__(self): | |
| super().__init__() | |
| self.node_title = "MIDI to Freq (Hz)" | |
| self.inputs = { | |
| 'midi_note_in': 'signal', # MIDI note number (0-127) | |
| 'velocity_in': 'signal' # MIDI velocity (0.0 to 1.0) | |
| } | |
| self.outputs = { | |
| 'frequency_out': 'signal', | |
| 'amplitude_out': 'signal' | |
| } | |
| self.output_freq = 0.0 | |
| self.output_amp = 0.0 | |
| self.current_note = 0 | |
| self.midi_offset = 0 | |
| def _midi_to_freq(self, midi_note): | |
| """Converts integer MIDI note number to frequency (Hz).""" | |
| if midi_note <= 0: | |
| return 0.0 | |
| # Clamp to reasonable range for calculation | |
| midi_note = np.clip(midi_note, 0, 127) | |
| # f = 440 * 2^((N - 69)/12) | |
| exponent = (midi_note - A4_MIDI) / 12.0 | |
| return float(A4_FREQ * math.pow(2, exponent)) | |
| def _get_note_name(self, midi_note): | |
| """Helper to get note name and octave for display.""" | |
| note_name_map = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"] | |
| note = int(midi_note) | |
| note_name = note_name_map[note % 12] | |
| octave = note // 12 - 1 | |
| return f"{note_name}{octave}" | |
| def step(self): | |
| # 1. Get raw inputs | |
| note_in = self.get_blended_input('midi_note_in', 'sum') or 0.0 | |
| amp_in = self.get_blended_input('velocity_in', 'sum') or 0.0 | |
| # 2. Quantize Note Input | |
| # Note numbers are integers; anything less than 0.5 is treated as 'off' | |
| if amp_in > 0.05 and note_in >= 0: | |
| self.current_note = int(round(note_in)) | |
| else: | |
| self.current_note = 0 | |
| # 3. Calculate Frequency | |
| self.output_freq = self._midi_to_freq(self.current_note) | |
| # 4. Calculate Amplitude | |
| # Amp is just the velocity signal, clamped and smoothed | |
| self.output_amp = np.clip(amp_in, 0.0, 1.0) | |
| def get_output(self, port_name): | |
| if port_name == 'frequency_out': | |
| # Output frequency only if amplitude is high enough | |
| return self.output_freq if self.output_amp > 0.05 else 0.0 | |
| elif port_name == 'amplitude_out': | |
| return self.output_amp | |
| return None | |
| def get_display_image(self): | |
| w, h = 96, 48 | |
| img = np.zeros((h, w, 3), dtype=np.uint8) | |
| note = self.current_note | |
| freq = self.output_freq | |
| amp = self.output_amp | |
| # Color based on activity | |
| if freq > 0.0: | |
| fill_color = (0, 150, 255) # Active Cyan | |
| text_color = (0, 0, 0) | |
| note_label = self._get_note_name(note) | |
| else: | |
| fill_color = (50, 50, 50) | |
| text_color = (150, 150, 150) | |
| note_label = "OFF" | |
| cv2.rectangle(img, (0, 0), (w, h), fill_color, -1) | |
| # Draw Note Label | |
| cv2.putText(img, note_label, (w//4, h//3), cv2.FONT_HERSHEY_SIMPLEX, 0.6, text_color, 2, cv2.LINE_AA) | |
| # Draw Frequency | |
| cv2.putText(img, f"{freq:.1f} Hz", (w//4, h//3 + 18), cv2.FONT_HERSHEY_SIMPLEX, 0.4, text_color, 1, cv2.LINE_AA) | |
| # Draw Amplitude Bar | |
| bar_w = int(amp * (w - 10)) | |
| cv2.rectangle(img, (5, h - 10), (5 + bar_w, h - 5), (255, 255, 255), -1) | |
| img = np.ascontiguousarray(img) | |
| return QtGui.QImage(img.data, w, h, 3*w, QtGui.QImage.Format.Format_BGR888) | |
| def get_config_options(self): | |
| return [ | |
| ("MIDI Note Offset", "midi_offset", self.midi_offset, None), | |
| ] |