-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'main' of github.com:PhilippThoelke/goofi-pipe
- Loading branch information
Showing
10 changed files
with
333 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
import time | ||
|
||
import numpy as np | ||
from oscpy.server import OSCThreadServer | ||
|
||
from goofi.data import Data, DataType | ||
from goofi.node import Node | ||
from goofi.params import IntParam, StringParam | ||
|
||
|
||
class OSCIn(Node): | ||
def config_params(): | ||
return { | ||
"osc": { | ||
"address": StringParam("0.0.0.0"), | ||
"port": IntParam(9000, 0, 65535), | ||
}, | ||
"common": {"autotrigger": True}, | ||
} | ||
|
||
def config_output_slots(): | ||
return {"message": DataType.TABLE} | ||
|
||
def setup(self): | ||
self.server = OSCThreadServer(advanced_matching=True) | ||
self.server.listen(address=self.params.osc.address.value, port=self.params.osc.port.value, default=True) | ||
|
||
# bind to possible addresses of depth 10 (is there a better way to do this?) | ||
for i in range(1, 11): | ||
self.server.bind(b"/*" * i, self.callback, get_address=True) | ||
|
||
self.messages = {} | ||
|
||
def callback(self, address, *args): | ||
if len(args) > 1: | ||
raise ValueError( | ||
"For now the OSCIn node only support a single argument per message. " | ||
"Please open an issue if you need more (https://github.com/PhilippThoelke/goofi-pipe/issues)." | ||
) | ||
|
||
val = args[0] | ||
if isinstance(val, bytes): | ||
val = Data(DataType.STRING, val.decode(), {}) | ||
else: | ||
val = Data(DataType.ARRAY, np.array([val]), {}) | ||
|
||
self.messages[address.decode()] = val | ||
|
||
def process(self): | ||
if len(self.messages) == 0: | ||
return None | ||
|
||
data = self.messages | ||
meta = {} | ||
self.messages = {} | ||
|
||
return {"message": (data, meta)} | ||
|
||
def osc_address_changed(self, address): | ||
self.server.stop_all() | ||
self.server.terminate_server() | ||
self.server.join_server() | ||
self.setup() | ||
|
||
def osc_port_changed(self, port): | ||
self.server.stop_all() | ||
self.server.terminate_server() | ||
self.server.join_server() | ||
self.setup() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
from goofi.data import Data, DataType | ||
from goofi.node import Node | ||
from goofi.params import BoolParam | ||
|
||
|
||
class StringAwait(Node): | ||
def config_input_slots(): | ||
return {"message": DataType.STRING, "trigger": DataType.ARRAY} | ||
|
||
def config_output_slots(): | ||
return {"out": DataType.STRING} | ||
|
||
def config_params(): | ||
return { | ||
"string_await": { | ||
"require_change": BoolParam(True, doc="Only output when the message changes, and we have an unconsumed trigger") | ||
} | ||
} | ||
|
||
def setup(self): | ||
self.last_message = None | ||
|
||
def process(self, message: Data, trigger: Data): | ||
if trigger is None or message is None: | ||
return | ||
|
||
if self.params.string_await.require_change.value and self.last_message == message.data: | ||
return | ||
|
||
self.input_slots["trigger"].clear() | ||
|
||
self.last_message = message.data | ||
return {"out": (message.data, message.meta)} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
import threading | ||
|
||
from goofi.data import Data, DataType | ||
from goofi.node import Node | ||
from goofi.params import IntParam, StringParam | ||
|
||
|
||
class MidiCCout(Node): | ||
def config_input_slots(): | ||
return { | ||
"cc1": DataType.ARRAY, | ||
"cc2": DataType.ARRAY, | ||
"cc3": DataType.ARRAY, | ||
"cc4": DataType.ARRAY, | ||
"cc5": DataType.ARRAY, | ||
} | ||
|
||
def config_output_slots(): | ||
return {"midi_status": DataType.STRING} | ||
|
||
def config_params(): | ||
try: | ||
import mido | ||
|
||
available_ports = mido.get_output_names() | ||
except Exception as e: | ||
print(f"Error getting MIDI ports: {e}") | ||
available_ports = [] | ||
|
||
return { | ||
"MIDI": { | ||
"port_name": StringParam("", options=available_ports, doc="MIDI output port name"), | ||
"channel": IntParam(1, 1, 15, doc="MIDI channel"), | ||
"cc1": IntParam(1, 0, 127, doc="MIDI CC number for input 1"), | ||
"cc2": IntParam(2, 0, 127, doc="MIDI CC number for input 2"), | ||
"cc3": IntParam(3, 0, 127, doc="MIDI CC number for input 3"), | ||
"cc4": IntParam(4, 0, 127, doc="MIDI CC number for input 4"), | ||
"cc5": IntParam(5, 0, 127, doc="MIDI CC number for input 5"), | ||
} | ||
} | ||
|
||
def send_cc(self, outport, cc_number, value, channel): | ||
"""Send a MIDI CC message.""" | ||
# convert cc_number to int | ||
cc_number = int(cc_number) | ||
value = int(value) | ||
outport.send(self.mido.Message("control_change", control=cc_number, value=value, channel=channel)) | ||
|
||
def setup(self): | ||
import mido | ||
|
||
self.mido = mido | ||
|
||
def process(self, cc1: Data, cc2: Data, cc3: Data, cc4: Data, cc5: Data): | ||
port_name = self.params.MIDI["port_name"].value | ||
channel = self.params.MIDI["channel"].value | ||
channel = channel - 1 # MIDI channels are 0-indexed | ||
cc_numbers = [ | ||
self.params.MIDI["cc1"].value, | ||
self.params.MIDI["cc2"].value, | ||
self.params.MIDI["cc3"].value, | ||
self.params.MIDI["cc4"].value, | ||
self.params.MIDI["cc5"].value, | ||
] | ||
cc_data = [cc1, cc2, cc3, cc4, cc5] | ||
|
||
outport = self.mido.open_output(port_name) if port_name != "goofi" else self.goofi_port | ||
|
||
alert_on = False | ||
try: | ||
threads = [] | ||
for i, data in enumerate(cc_data): | ||
if data is not None and len(data.data) > 0: | ||
for value in data.data: | ||
if value < 0 or value > 127: | ||
print(f"Value outside of MIDI 0-127 range: {value}") | ||
alert_on = True | ||
error_message = f"Value outside of MIDI 0-127 range: {value}" | ||
pass | ||
t = threading.Thread(target=self.send_cc, args=(outport, cc_numbers[i], value, channel)) | ||
t.start() | ||
threads.append(t) | ||
|
||
for t in threads: | ||
t.join() | ||
finally: | ||
if port_name != "goofi": | ||
outport.close() # Ensure that the MIDI port is closed when done | ||
|
||
if alert_on: | ||
return {"midi_status": (f"CC messages sent with errors\n{error_message}", cc1.meta)} | ||
else: | ||
return {"midi_status": ("CC messages sent successfully", cc1.meta)} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
import threading | ||
import time | ||
|
||
import numpy as np | ||
|
||
from goofi.data import Data, DataType | ||
from goofi.node import Node | ||
from goofi.params import BoolParam, FloatParam, IntParam, StringParam | ||
|
||
|
||
def hz_to_midi(hz): | ||
"""Convert a frequency in Hz to a MIDI note number.""" | ||
return int(69 + 12 * np.log2(hz / 440.0)) | ||
|
||
|
||
class MidiOut(Node): | ||
def config_input_slots(): | ||
return {"note": DataType.ARRAY, "velocity": DataType.ARRAY, "duration": DataType.ARRAY} | ||
|
||
def config_output_slots(): | ||
return {"midi_status": DataType.STRING} | ||
|
||
def config_params(): | ||
try: | ||
import mido | ||
|
||
available_ports = mido.get_output_names() | ||
except Exception as e: | ||
print(f"Error getting MIDI ports: {e}") | ||
available_ports = [] | ||
|
||
return { | ||
"MIDI": { | ||
"port_name": StringParam("", options=available_ports, doc="MIDI output port name"), | ||
"channel": IntParam(0, 0, 15, doc="MIDI channel"), | ||
"play_mode": StringParam("simultaneous", options=["simultaneous", "sequential"], doc="Play mode"), | ||
"default_velocity": IntParam(100, 0, 127, doc="Default MIDI velocity"), | ||
"default_duration": FloatParam(0.1, 0.001, 10, doc="Default note duration in seconds"), | ||
"hz_input": BoolParam(False, doc="Interpret input as Hz"), | ||
} | ||
} | ||
|
||
def play_note(self, outport, n, v, d, channel): | ||
"""Thread function to play a note.""" | ||
outport.send(self.mido.Message("note_on", note=n, velocity=v, channel=channel)) | ||
time.sleep(d) | ||
outport.send(self.mido.Message("note_off", note=n, velocity=0, channel=channel)) | ||
|
||
def setup(self): | ||
import mido | ||
|
||
self.mido = mido | ||
|
||
def process(self, note: Data, velocity: Data, duration: Data): | ||
if note is None or len(note.data) == 0: | ||
return None | ||
|
||
# Convert Hz to MIDI if required | ||
midi_notes = [hz_to_midi(n) if self.params.MIDI["hz_input"].value else n for n in note.data] | ||
|
||
# Set up velocities | ||
if velocity is None: | ||
velocities = [self.params.MIDI["default_velocity"].value] * len(midi_notes) | ||
else: | ||
# Fill in default velocities where necessary | ||
velocities = [int(v) if v is not None else self.params.MIDI["default_velocity"].value for v in velocity.data] | ||
|
||
# Set up durations | ||
if duration is None: | ||
durations = [self.params.MIDI["default_duration"].value] * len(midi_notes) | ||
else: | ||
# Fill in default durations where necessary | ||
durations = [float(d) if d is not None else self.params.MIDI["default_duration"].value for d in duration.data] | ||
|
||
port_name = self.params.MIDI["port_name"].value | ||
channel = self.params.MIDI["channel"].value | ||
play_mode = self.params.MIDI["play_mode"].value | ||
|
||
outport = self.mido.open_output(port_name) # Open the MIDI port once | ||
alert_on = False | ||
try: | ||
if play_mode == "simultaneous": | ||
threads = [] | ||
for n, v, d in zip(midi_notes, velocities, durations): | ||
if n > 127 or n < 0: | ||
print("Note outside of MIDI 0-127 range") | ||
alert_on = True | ||
error_message = "Note outside of MIDI 0-127 range" | ||
pass | ||
if v > 127 or v < 0: | ||
print("Velocity outside of MIDI 0-127 range") | ||
alert_on = True | ||
error_message = "Velocity outside of MIDI 0-127 range" | ||
pass | ||
t = threading.Thread(target=self.play_note, args=(outport, n, v, d, channel)) | ||
t.start() | ||
threads.append(t) | ||
|
||
# Wait for all threads to complete | ||
for t in threads: | ||
t.join() | ||
elif play_mode == "sequential": | ||
for n, v, d in zip(midi_notes, velocities, durations): | ||
if n > 127 or n < 0: | ||
print("Note outside of MIDI 0-127 range") | ||
alert_on = True | ||
error_message = "Note outside of MIDI 0-127 range" | ||
pass | ||
if v > 127 or v < 0: | ||
print("Velocity outside of MIDI 0-127 range") | ||
alert_on = True | ||
error_message = "Velocity outside of MIDI 0-127 range" | ||
pass | ||
self.play_note(outport, n, v, d, channel) | ||
finally: | ||
outport.close() # Ensure that the MIDI port is closed when done | ||
|
||
if alert_on: | ||
return {"midi_status": (f"Notes sent successfully\n{error_message}", note.meta)} | ||
else: | ||
return {"midi_status": ("Notes sent successfully", note.meta)} |
Oops, something went wrong.