MidiOut
Send MIDI: note-on for a number that appears, note-off for one that goes.
notes
value
- Type name
signal:MidiOut- Plane
signal- Tags
outputmidi- Language
python- Tier
in-process- Bundle
signal- Source
node-bundles/signal/midi_out.py- Availability
- available
Slots
| Slot | Direction | Type | |
|---|---|---|---|
notes | input | ARRAY | |
value | input | ARRAY |
Parameters
midi
| Name | Type | Default | Range | Doc |
|---|---|---|---|---|
port | string | "" | Which port to send on. refreshable | |
channel | int | 1 | 1 … 16 | Which channel to send on. |
velocity | int | 100 | 1 … 127 | How hard every note is struck. |
controller | int | 1 | 0 … 127 | Which controller `value` is sent as. |
common
| Name | Type | Default | Range | Doc |
|---|---|---|---|---|
autotrigger | bool | false | Run on the node's own schedule, instead of waiting for an input frame. Turn this on for sources; leave it off for transforms driven by their input. | |
max_frequency | float | 0 | 0 … 100 | Rate cap for this node, read through `frequency_mode`. 0 means uncapped — the node runs as often as the scheduler and its inputs allow. |
frequency_mode | string | updates_per_second | updates_per_second | seconds_per_update | How to read `max_frequency`: as a rate in Hz (updates per second), or as a period in seconds between updates — convenient for very slow nodes. |
Source
The current source of this node, as it stands in the goofi repository at node-bundles/signal/midi_out.py.
"""MidiOut — sends MIDI for the notes being held and for one controller value.
`notes` is read as the set of note numbers held RIGHT NOW: a number that appears sends note-on
and a number that disappears sends note-off, so nothing is sent twice and nothing hangs. What is
still held is released when the node stops.
"""
import mido
import numpy as np
import goofi
class MidiOut(goofi.Node):
"""Send MIDI: note-on for a number that appears, note-off for one that goes."""
TAGS = ["output", "midi"]
INPUTS = {
"notes": goofi.InputSlot(goofi.DataType.ARRAY),
"value": goofi.InputSlot(goofi.DataType.ARRAY),
}
PARAMS = {
"midi": {
"port": goofi.StringParam("", options=[""], refresh=True, doc="Which port to send on."),
"channel": goofi.IntParam(1, 1, 16, doc="Which channel to send on."),
"velocity": goofi.IntParam(100, 1, 127, doc="How hard every note is struck."),
"controller": goofi.IntParam(1, 0, 127, doc="Which controller `value` is sent as."),
}
}
def setup(self):
self.port = None
self.opened = None
self.held = set()
self.last_cc = None
def refresh_midi_port(self):
return [""] + sorted(mido.get_output_names())
def process(self, notes, value):
p = self.params.midi
if p.port != self.opened:
self.stop()
if p.port:
self.port = mido.open_output(p.port)
self.opened = p.port
if self.port is None:
return None
channel = p.channel - 1
if notes is not None:
wanted = {int(round(n)) for n in np.asarray(notes.data).ravel() if 0 <= n <= 127}
for note in sorted(self.held - wanted):
self.port.send(mido.Message("note_off", note=note, channel=channel))
for note in sorted(wanted - self.held):
self.port.send(mido.Message("note_on", note=note, velocity=p.velocity, channel=channel))
self.held = wanted
if value is not None:
level = int(round(float(np.asarray(value.data).ravel()[0]) * 127))
level = max(0, min(127, level))
if level != self.last_cc:
self.port.send(mido.Message("control_change", control=p.controller, value=level, channel=channel))
self.last_cc = level
return None
def stop(self):
if self.port is not None:
for note in sorted(self.held):
self.port.send(mido.Message("note_off", note=note, channel=self.params.midi.channel - 1))
self.port.close()
self.port = None
self.held = set()This reference describes goofi 3.1.0(537cd394), generated from a running instance on 2026-09-06.