Skip to content

Instantly share code, notes, and snippets.

@herronelou
Created May 26, 2025 04:56
Show Gist options
  • Save herronelou/68483020f26e185d39498fa1944538d5 to your computer and use it in GitHub Desktop.
Save herronelou/68483020f26e185d39498fa1944538d5 to your computer and use it in GitHub Desktop.
Nuke Audio generator
set cut_paste_input [stack 0]
version 14.1 v5
push $cut_paste_input
Expression {
channel0 {rgba.red -rgba.green rgba.blue}
name Expression5
selected true
xpos 2349
ypos -55
addUserKnob {20 User}
addUserKnob {3 sampling l "Sampling Rate"}
sampling 44100
addUserKnob {1 song l Song}
song "E4, D#4, E4, D#4, E4, B3, D4, C4, A3, (A3 C3), (A3 E2), (A3 A2), (A3 C3), \nE3, A3, B3, (B3 E2), (B3 G#2), (B3 E3), \nG#3, B3, C4, (C4 E2), (C4 A2), (C4 E3), \nE4, D#4, E4, D#4, E4, B3, D4, C4, A3, (A3 E2), (A3 A2), (A3 C3), \nE3, A3, B3, (B3 E2), (B3 A2), (B3 G#2), (B3 E3), \nC4, B3, A3, (A3 E2), (A3 A2), A3 ,\n\nE4, D#4, E4, D#4, E4, B3, D4, C4, A3,(A3 E2), (A3 A2), (A3 C3), \nE3, A3, B3, (B3 E2), (B3 G#2), (B3 E3),\nG#3, B3, C4,(C4 E2), E3,\nE4, D#4, E4, D#4, E4, B3, D4, C4, A3, (A3 E2), A2, C3, E3, A3, \n B3, (B3 E2), G#2, E3, C4, B3, A3, (A3 E2), A2, B3, C4, D4, E4, (E4 G2), (E4 C3), (E4 G3), F4, E4, \nD4, (D4 G2), (D4 B2), (D4 F3), E4, D4, C4, (C4 E2), (C4 A2), (C4 E3), D4, C4, B3, (B3 E2), E3, E3, E4, E3, E4, E4,, \nE4, D#4, E4, D#4, E4,B3, D4, C4, A3,(A3 E2), A2, C3, E3, A3, B3, (B3 E2), G#2, E3, G#3, B3, C4, (C4 E2), A2, E3,\nE4, D#4, E4, D#4, E4, B3, D4, C4, A3, (A3 E2), A2, C3, E3, A2, B3, (B3 E2), G#2, E3\n"
addUserKnob {22 generate l Generate T "import math\nimport re\n\n# --- Configuration ---\nA4_FREQ = 440.0 # Frequency of A4 note\n\n# Semitone values relative to C in an octave (C=0, C#=1, ..., B=11)\n# This map helps convert note names (like 'A', 'C') to a base semitone value.\nNOTE_TO_SEMITONE_MAP = \{\n 'C': 0, 'D': 2, 'E': 4, 'F': 5, 'G': 7, 'A': 9, 'B': 11\n\}\n# Reference for A4: Octave 4, Note 'A'\nA4_OCTAVE = 4\nA4_SEMITONE_OFFSET_IN_OCTAVE = NOTE_TO_SEMITONE_MAP\['A'] # Should be 9\n# Calculate the number of semitones A4 is from C0 (C in octave 0)\nNUM_SEMITONES_A4_FROM_C0 = A4_OCTAVE * 12 + A4_SEMITONE_OFFSET_IN_OCTAVE\n\n\ndef get_note_frequency(note_str: str) -> float:\n \"\"\"\n Calculates the frequency of a given musical note string.\n Args:\n note_str: The note string, e.g., \"A4\", \"C#3\", \"B#2\".\n Handles sharps (#). B# is treated as C of the next octave,\n E# is treated as F of the same octave.\n Returns:\n The frequency of the note in Hz.\n Raises:\n ValueError: If the note format is invalid.\n \"\"\"\n note_str = note_str.strip()\n # Regex for notes like C4, A#3. Octaves 0-8. Sharp is optional.\n # Example: \"A#3\" -> name='A', sharp_char='#', octave_str='3'\n # Example: \"C4\" -> name='C', sharp_char='', octave_str='4'\n match = re.fullmatch(r'(\[A-G])(\[#]?)(\[0-8])', note_str)\n if not match:\n raise ValueError(\n f\"Invalid note format: '\{note_str\}'. Expected format e.g., 'A4', 'C#3'.\"\n )\n\n name, sharp_char, octave_str = match.groups()\n octave = int(octave_str)\n\n semitone_offset = NOTE_TO_SEMITONE_MAP\[name]\n\n if sharp_char == '#':\n semitone_offset += 1\n \n # Normalize semitone offset and octave for cases like B# or E#\n # If semitone_offset becomes 12 (e.g., B#), it's C (0) of the next octave.\n # If semitone_offset becomes 5 due to E# (E=4 + #=1), it's F (5), no octave change needed unless it goes >=12.\n if semitone_offset >= 12:\n semitone_offset -= 12 # Wrap around to 0 (C)\n octave += 1\n \n # Calculate the total number of semitones from C0 for the current note\n num_semitones_current_note_from_C0 = octave * 12 + semitone_offset\n\n # Calculate the difference in semitones from A4\n semitone_diff_from_A4 = num_semitones_current_note_from_C0 - NUM_SEMITONES_A4_FROM_C0\n \n # Calculate frequency using the formula: f = f_ref * (2^(1/12))^n\n frequency = A4_FREQ * (2**(semitone_diff_from_A4 / 12.0))\n return frequency\n\n\ndef get_bin_for_frequency(frequency: float, fftsize: int, sampling_rate: float) -> int:\n \"\"\"\n Calculates the nearest FFT bin index for a given frequency.\n Args:\n frequency: The frequency in Hz.\n fftsize: The size of the FFT.\n sampling_rate: The sampling rate in Hz.\n Returns:\n The nearest FFT bin index.\n Raises:\n ValueError: If fftsize or sampling_rate are not positive.\n \"\"\"\n if fftsize <= 0:\n raise ValueError(\"FFT size must be positive.\")\n if sampling_rate <= 0:\n raise ValueError(\"Sampling rate must be positive.\")\n\n if frequency == 0: # Should generally not be the case for musical notes\n return 0\n \n # Frequency resolution: df = sampling_rate / fftsize\n # Bin index k for frequency f: f = k * df => k = f / df = f * fftsize / sampling_rate\n bin_index_float = frequency * fftsize / sampling_rate\n bin_index = round(bin_index_float)\n\n # Ensure bin_index is within the valid range \[0, fftsize - 1]\n # The FFT output has 'fftsize' bins, indexed 0 to fftsize-1.\n return max(0, min(int(bin_index), fftsize - 1))\n\n\ndef generate_expression(input_music_string: str, fftsize: int, sampling_rate: float) -> str:\n \"\"\"\n Generates the expression string from the input music notation.\n Args:\n input_music_string: The music string, e.g., \"A4, 0, (C4 E4 G4), B#2\".\n fftsize: The FFT size.\n sampling_rate: The sampling rate.\n Returns:\n The generated expression string.\n \"\"\"\n time_events_raw = input_music_string.split(',')\n time_sample_index = 0 # 'y' in the output expression\n conditions = \[]\n\n for time_event_str_raw in time_events_raw:\n time_event_str = time_event_str_raw.strip()\n \n # If the part after split is empty (e.g. \"A1,\"), it's an empty time step.\n # It will not generate notes, but time_sample_index will still advance.\n if not time_event_str:\n time_sample_index += 1\n continue\n\n notes_for_current_sample = \[]\n\n if time_event_str == '0':\n # Silence for this time sample, no notes to add.\n pass\n elif time_event_str.startswith('(') and time_event_str.endswith(')'):\n # Chord: (Note1 Note2 Note3)\n chord_content = time_event_str\[1:-1].strip()\n if chord_content: # Ensure not an empty chord \"()\"\n # Notes within a chord are space-separated\n note_strs_in_chord = chord_content.split()\n for note_str in note_strs_in_chord:\n notes_for_current_sample.append(note_str.strip())\n else:\n # Single note\n notes_for_current_sample.append(time_event_str)\n\n for note_to_process in notes_for_current_sample:\n try:\n frequency = get_note_frequency(note_to_process)\n bin_index = get_bin_for_frequency(frequency, fftsize, sampling_rate)\n # Add condition: x == bin_index AND y == time_sample_index\n conditions.append(f\"x==\{bin_index\}&&y==\{time_sample_index\}\")\n except ValueError as e:\n # Print a warning and skip this invalid note.\n print(f\"Warning: Skipping invalid note '\{note_to_process\}' at time sample \{time_sample_index\}: \{e\}\")\n \n time_sample_index += 1 # Move to the next time sample\n\n # Join all conditions with OR operator\n return \"||\".join(conditions)\n\nnode = nuke.thisNode()\nsong = node\['song'].value()\nresult = generate_expression(song, (node.width()-1)*2, node\['sampling'].value())\nnode\['expr0'].setValue(result)" +STARTLINE}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment