MidiOut

Send MIDI: note-on for a number that appears, note-off for one that goes.

MidiOut py
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

SlotDirectionType
notesinputARRAY
valueinputARRAY

Parameters

midi

NameTypeDefaultRangeDoc
portstring""Which port to send on. refreshable
channelint11 … 16Which channel to send on.
velocityint1001 … 127How hard every note is struck.
controllerint10 … 127Which controller `value` is sent as.

common

NameTypeDefaultRangeDoc
autotriggerboolfalseRun 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_frequencyfloat00 … 100Rate cap for this node, read through `frequency_mode`. 0 means uncapped — the node runs as often as the scheduler and its inputs allow.
frequency_modestringupdates_per_secondupdates_per_second | seconds_per_updateHow 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.

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()

← All nodes

This reference describes goofi 3.1.0(537cd394), generated from a running instance on 2026-09-06.