cs-midi-docs/docs/manual/02-midi/01-midi-tutorial.md

4.1 KiB

MIDI Tutorial

Original: MIDI Tutorial

MIDI Interfaces

A MIDI interface handles the transport layer — BLE, USB, serial, or WiFi. Instantiate one before sending or receiving:

BluetoothMIDI_Interface midi;  // BLE MIDI via BTstack

Initialize with midi.begin() (standalone) or let Control_Surface.begin() handle it.

MIDI Addresses

MIDIAddress combines three components: address (0-127), channel (1-16), and cable (1-16, default 1).

MIDIAddress note = {MIDI_Notes::C[4], Channel_1};
MIDIAddress cc   = {MIDI_CC::Channel_Volume, Channel_2};
MIDIAddress pc   = {MIDI_PC::Harpsichord, Channel_10};

Predefined constants:

  • MIDI_Notes::C[4] — note number 60 (middle C)
  • MIDI_CC::Channel_Volume — CC 7
  • MIDI_PC::Harpsichord — program 7

Channel defaults to Channel_1, cable defaults to Cable_1.

Sending Messages

All interface objects inherit from MIDI_Sender and provide:

midi.sendNoteOn(address, velocity);
midi.sendNoteOff(address, velocity);
midi.sendControlChange(address, value);
midi.sendProgramChange(address);
midi.sendPitchBend(channel, value);    // 14-bit (0-16383)
midi.sendChannelPressure(channel, pressure);
midi.sendKeyPressure(address, pressure);

System messages:

midi.sendTimingClock();
midi.sendStart();
midi.sendContinue();
midi.sendStop();
midi.sendSysEx(data);  // uint8_t array with F0...F7

Call midi.sendNow() to flush any buffered output.

Receiving Messages

MIDI_Callbacks

Subclass MIDI_Callbacks and override the handlers you need:

struct MyCallbacks : MIDI_Callbacks {
    void onChannelMessage(MIDI_Interface &, ChannelMessage msg) override {
        uint8_t type = static_cast<uint8_t>(msg.getMessageType());
        uint8_t d1 = msg.getData1();
        uint8_t d2 = msg.getData2();
        Channel ch = msg.getChannel();
        (void)type; (void)d1; (void)d2; (void)ch;
    }
    void onSysExMessage(MIDI_Interface &, SysExMessage msg) override {
        const uint8_t *data = msg.data;
        uint16_t len = msg.length;
        (void)data; (void)len;
    }
    void onRealTimeMessage(MIDI_Interface &, RealTimeMessage msg) override {
        (void)msg;
    }
} callbacks;

// Register: midi.setCallbacks(callbacks);

FineGrainedMIDI_Callbacks

For per-message-type dispatch, use the CRTP variant:

struct MyFineCallbacks : FineGrainedMIDI_Callbacks<MyFineCallbacks> {
    void onNoteOn(Channel ch, uint8_t note, uint8_t vel, Cable cable) {
        (void)ch; (void)note; (void)vel; (void)cable;
    }
    void onNoteOff(Channel ch, uint8_t note, uint8_t vel, Cable cable) {
        (void)ch; (void)note; (void)vel; (void)cable;
    }
    void onControlChange(Channel ch, uint8_t cc, uint8_t val, Cable cable) {
        (void)ch; (void)cc; (void)val; (void)cable;
    }
} fineCallbacks;

Only override the methods you need — unoverridden methods are no-ops.

Routing Messages

Default routing

When using Control_Surface.begin(), the singleton automatically connects output elements to the default MIDI interface and dispatches incoming messages to input elements.

Manual routing with pipes

MIDI_Pipe connects a source to a sink. Use operators for concise wiring:

MIDI_PipeFactory<2> pipes;
midi1 >> pipes >> midi2;  // midi1 output → midi2
midi2 >> pipes >> midi1;  // midi2 output → midi1

Bidirectional:

BidirectionalMIDI_PipeFactory<1> bpipes;
midi1 | bpipes | midi2;  // full duplex

Filtering

Subclass MIDI_Pipe and override mapForwardMIDI to filter or transform:

struct NoteOnly : MIDI_Pipe {
    void mapForwardMIDI(ChannelMessage msg) override {
        if (msg.getMessageType() == MIDIMessageType::NoteOn ||
            msg.getMessageType() == MIDIMessageType::NoteOff)
            sourceMIDItoSink(msg);
    }
    void mapForwardMIDI(SysExMessage) override {}
    void mapForwardMIDI(SysCommonMessage) override {}
    void mapForwardMIDI(RealTimeMessage msg) override {
        sourceMIDItoSink(msg);
    }
};