Add support for MiDi output

Implements godotengine/godot-proposals#2321

Currently the API is very simple, a new method is created OS.send_midi that accepts a InputEventMIDI and sends it to the midi device specified in the event. Output device IDs can be found in OS.get_connected_midi_outputs. To use this API you first need to open MIDI with OS.open_midi_inputs which now opens both inputs and outputs.
This commit is contained in:
Rob Blanckaert 2024-10-11 00:51:12 -07:00
parent 92e51fca72
commit df09a2afa4
15 changed files with 225 additions and 11 deletions

View file

@ -225,6 +225,10 @@ PackedStringArray OS::get_connected_midi_inputs() {
return ::OS::get_singleton()->get_connected_midi_inputs();
}
PackedStringArray OS::get_connected_midi_outputs() {
return ::OS::get_singleton()->get_connected_midi_outputs();
}
void OS::open_midi_inputs() {
::OS::get_singleton()->open_midi_inputs();
}
@ -232,6 +236,9 @@ void OS::open_midi_inputs() {
void OS::close_midi_inputs() {
::OS::get_singleton()->close_midi_inputs();
}
void OS::send_midi(Ref<InputEventMIDI> p_event) {
::OS::get_singleton()->send_midi(p_event);
}
void OS::set_use_file_access_save_and_swap(bool p_enable) {
FileAccess::set_backup_save(p_enable);
@ -606,8 +613,10 @@ void OS::_bind_methods() {
ClassDB::bind_method(D_METHOD("get_entropy", "size"), &OS::get_entropy);
ClassDB::bind_method(D_METHOD("get_system_ca_certificates"), &OS::get_system_ca_certificates);
ClassDB::bind_method(D_METHOD("get_connected_midi_inputs"), &OS::get_connected_midi_inputs);
ClassDB::bind_method(D_METHOD("get_connected_midi_outputs"), &OS::get_connected_midi_outputs);
ClassDB::bind_method(D_METHOD("open_midi_inputs"), &OS::open_midi_inputs);
ClassDB::bind_method(D_METHOD("close_midi_inputs"), &OS::close_midi_inputs);
ClassDB::bind_method(D_METHOD("send_midi", "event"), &OS::send_midi);
ClassDB::bind_method(D_METHOD("alert", "text", "title"), &OS::alert, DEFVAL("Alert!"));
ClassDB::bind_method(D_METHOD("crash", "message"), &OS::crash);

View file

@ -148,8 +148,10 @@ public:
String get_system_ca_certificates();
virtual PackedStringArray get_connected_midi_inputs();
virtual PackedStringArray get_connected_midi_outputs();
virtual void open_midi_inputs();
virtual void close_midi_inputs();
virtual void send_midi(Ref<InputEventMIDI> p_event);
void set_low_processor_usage_mode(bool p_enabled);
bool is_in_low_processor_usage_mode() const;

View file

@ -1829,6 +1829,46 @@ int InputEventMIDI::get_controller_value() const {
return controller_value;
}
PackedByteArray InputEventMIDI::get_midi_bytes() const {
PackedByteArray result;
result.append(static_cast<uint8_t>(static_cast<int>(message) << 4 | (channel & 0xF)));
switch (message) {
case MIDIMessage::NOTE_OFF:
case MIDIMessage::NOTE_ON:
result.append(pitch);
result.append(velocity);
break;
case MIDIMessage::AFTERTOUCH:
result.append(pitch);
result.append(pressure);
break;
case MIDIMessage::CONTROL_CHANGE:
result.append(controller_number);
result.append(controller_value);
break;
case MIDIMessage::PITCH_BEND:
result.append(pitch | 0x80);
result.append(pitch >> 7);
break;
case MIDIMessage::SONG_POSITION_POINTER:
result.resize(3);
break;
case MIDIMessage::PROGRAM_CHANGE:
result.append(instrument);
break;
case MIDIMessage::CHANNEL_PRESSURE:
result.append(pressure);
break;
case MIDIMessage::QUARTER_FRAME:
case MIDIMessage::SONG_SELECT:
result.resize(2);
break;
default:
break;
}
return result;
}
String InputEventMIDI::as_text() const {
return vformat(RTR("MIDI Input on Channel=%s Message=%s"), itos(channel), itos((int64_t)message));
}

View file

@ -572,6 +572,8 @@ public:
void set_controller_value(const int p_controller_value);
int get_controller_value() const;
PackedByteArray get_midi_bytes() const;
virtual String as_text() const override;
virtual String to_string() override;

View file

@ -202,3 +202,7 @@ void MIDIDriver::Parser::parse_fragment(uint8_t p_fragment) {
PackedStringArray MIDIDriver::get_connected_inputs() const {
return connected_input_names;
}
PackedStringArray MIDIDriver::get_connected_outputs() const {
return connected_output_names;
}

View file

@ -31,6 +31,7 @@
#ifndef MIDI_DRIVER_H
#define MIDI_DRIVER_H
#include "core/input/input.h"
#include "core/typedefs.h"
#include "core/variant/variant.h"
@ -98,6 +99,7 @@ protected:
};
PackedStringArray connected_input_names;
PackedStringArray connected_output_names;
public:
static MIDIDriver *get_singleton();
@ -106,9 +108,13 @@ public:
virtual ~MIDIDriver() = default;
virtual Error open() = 0;
virtual Error send(Ref<InputEventMIDI> p_event) {
return Error::FAILED;
}
virtual void close() = 0;
PackedStringArray get_connected_inputs() const;
PackedStringArray get_connected_outputs() const;
};
#endif // MIDI_DRIVER_H

View file

@ -563,6 +563,15 @@ PackedStringArray OS::get_connected_midi_inputs() {
ERR_FAIL_V_MSG(list, vformat("MIDI input isn't supported on %s.", OS::get_singleton()->get_name()));
}
PackedStringArray OS::get_connected_midi_outputs() {
if (MIDIDriver::get_singleton()) {
return MIDIDriver::get_singleton()->get_connected_outputs();
}
PackedStringArray list;
ERR_FAIL_V_MSG(list, vformat("MIDI output isn't supported on %s.", OS::get_singleton()->get_name()));
}
void OS::open_midi_inputs() {
if (MIDIDriver::get_singleton()) {
MIDIDriver::get_singleton()->open();
@ -579,6 +588,10 @@ void OS::close_midi_inputs() {
}
}
Error OS::send_midi(Ref<InputEventMIDI> p_event) {
return MIDIDriver::get_singleton()->send(p_event);
}
void OS::add_frame_delay(bool p_can_draw) {
const uint32_t frame_delay = Engine::get_singleton()->get_frame_delay();
if (frame_delay) {

View file

@ -153,8 +153,10 @@ public:
virtual String get_system_ca_certificates() { return ""; } // Concatenated certificates in PEM format.
virtual PackedStringArray get_connected_midi_inputs();
virtual PackedStringArray get_connected_midi_outputs();
virtual void open_midi_inputs();
virtual void close_midi_inputs();
virtual Error send_midi(Ref<InputEventMIDI> p_event);
virtual void alert(const String &p_alert, const String &p_title = "ALERT!");

View file

@ -243,7 +243,14 @@
<method name="get_connected_midi_inputs">
<return type="PackedStringArray" />
<description>
Returns an array of connected MIDI device names, if they exist. Returns an empty array if the system MIDI driver has not previously been initialized with [method open_midi_inputs]. See also [method close_midi_inputs].
Returns an array of connected MIDI input device names, if they exist. Returns an empty array if the system MIDI driver has not previously been initialized with [method open_midi_inputs]. See also [method close_midi_inputs].
[b]Note:[/b] This method is implemented on Linux, macOS and Windows.
</description>
</method>
<method name="get_connected_midi_outputs">
<return type="PackedStringArray" />
<description>
Returns an array of connected MIDI output device names, if they exist. Returns an empty array if the system MIDI driver has not previously been initialized with [method open_midi_inputs]. See also [method close_midi_inputs].
[b]Note:[/b] This method is implemented on Linux, macOS and Windows.
</description>
</method>
@ -712,6 +719,14 @@
On macOS (sandboxed applications only), this function clears list of user selected folders accessible to the application.
</description>
</method>
<method name="send_midi">
<return type="void" />
<param index="0" name="event" type="InputEventMIDI" />
<description>
Send the MiDi [param event] to the device specieid in the events device_id.
[b]Note:[/b] This method is implemented on Linux, macOS and Windows.
</description>
</method>
<method name="set_environment" qualifiers="const">
<return type="void" />
<param index="0" name="variable" type="String" />

View file

@ -87,19 +87,29 @@ Error MIDIDriverALSAMidi::open() {
if (name != nullptr) {
snd_rawmidi_t *midi_in;
int ret = snd_rawmidi_open(&midi_in, nullptr, name, SND_RAWMIDI_NONBLOCK);
snd_rawmidi_t *midi_out;
int ret = snd_rawmidi_open(&midi_in, &midi_out, name, SND_RAWMIDI_NONBLOCK);
if (ret >= 0) {
// Get display name.
if (midi_in != nullptr) {
snd_rawmidi_info_t *info;
snd_rawmidi_info_malloc(&info);
snd_rawmidi_info(midi_in, info);
connected_input_names.push_back(snd_rawmidi_info_get_name(info));
snd_rawmidi_info_free(info);
connected_inputs.push_back(InputConnection(device_index, midi_in));
// Only increment device_index for successfully connected devices.
device_index++;
}
if (midi_out != nullptr) {
snd_rawmidi_info_t *info;
snd_rawmidi_info_malloc(&info);
snd_rawmidi_info(midi_out, info);
connected_output_names.push_back(snd_rawmidi_info_get_name(info));
connected_outputs.push_back(midi_out);
snd_rawmidi_info_free(info);
}
}
}
if (name != nullptr) {
@ -129,6 +139,15 @@ void MIDIDriverALSAMidi::close() {
connected_input_names.clear();
}
Error MIDIDriverALSAMidi::send(Ref<InputEventMIDI> p_event) {
ERR_FAIL_COND_V(p_event.is_null(), ERR_INVALID_PARAMETER);
int device_id = p_event->get_device();
ERR_FAIL_INDEX_V(device_id, connected_outputs.size(), ERR_PARAMETER_RANGE_ERROR);
PackedByteArray packet = p_event->get_midi_bytes();
snd_rawmidi_write(connected_outputs[device_id], packet.ptrw(), packet.size());
return OK;
}
void MIDIDriverALSAMidi::lock() const {
mutex.lock();
}

View file

@ -63,6 +63,7 @@ class MIDIDriverALSAMidi : public MIDIDriver {
};
Vector<InputConnection> connected_inputs;
Vector<snd_rawmidi_t *> connected_outputs;
SafeFlag exit_thread;
@ -74,6 +75,7 @@ class MIDIDriverALSAMidi : public MIDIDriver {
public:
virtual Error open() override;
virtual void close() override;
virtual Error send(Ref<InputEventMIDI> p_event) override;
MIDIDriverALSAMidi();
virtual ~MIDIDriverALSAMidi();

View file

@ -75,6 +75,12 @@ Error MIDIDriverCoreMidi::open() {
return ERR_CANT_OPEN;
}
result = MIDIOutputPortCreate(client, CFSTR("Godot Output"), &port_out);
if (result != noErr) {
ERR_PRINT("MIDIOutputPortCreate failed, code: " + itos(result));
return ERR_CANT_OPEN;
}
int source_count = MIDIGetNumberOfSources();
int connection_index = 0;
for (int i = 0; i < source_count; i++) {
@ -99,6 +105,21 @@ Error MIDIDriverCoreMidi::open() {
}
}
int sink_count = MIDIGetNumberOfDestinations();
for (int i = 0; i < sink_count; i++) {
MIDIEndpointRef sink = MIDIGetDestination(i);
if (sink) {
CFStringRef nameRef = nullptr;
char name[256];
MIDIObjectGetStringProperty(sink, kMIDIPropertyDisplayName, &nameRef);
CFStringGetCString(nameRef, name, sizeof(name), kCFStringEncodingUTF8);
CFRelease(nameRef);
connected_output_names.push_back(name);
} else {
connected_output_names.push_back("ERROR");
}
}
return OK;
}
@ -114,18 +135,48 @@ void MIDIDriverCoreMidi::close() {
connected_sources.clear();
connected_input_names.clear();
connected_output_names.clear();
if (port_in != 0) {
MIDIPortDispose(port_in);
port_in = 0;
}
if (port_out != 0) {
MIDIPortDispose(port_out);
port_out = 0;
}
if (client != 0) {
MIDIClientDispose(client);
client = 0;
}
}
Error MIDIDriverCoreMidi::send(Ref<InputEventMIDI> p_event) {
ERR_FAIL_COND_V(p_event.is_null(), ERR_INVALID_PARAMETER);
ItemCount device_id = ItemCount(p_event->get_device());
ERR_FAIL_INDEX_V(device_id, MIDIGetNumberOfDestinations(), Error::ERR_PARAMETER_RANGE_ERROR);
MIDITimeStamp timestamp = 0;
Byte buffer[1024];
MIDIPacketList *packetlist = (MIDIPacketList *)buffer;
MIDIPacket *currentpacket = MIDIPacketListInit(packetlist);
PackedByteArray packet = p_event->get_midi_bytes();
currentpacket = MIDIPacketListAdd(
packetlist,
sizeof(buffer),
currentpacket,
timestamp,
packet.size(),
packet.ptr());
OSStatus status = MIDISend(port_out, MIDIGetDestination(device_id), packetlist);
if (status) {
return Error::FAILED;
}
return OK;
}
MIDIDriverCoreMidi::~MIDIDriverCoreMidi() {
close();
}

View file

@ -43,6 +43,7 @@
class MIDIDriverCoreMidi : public MIDIDriver {
MIDIClientRef client = 0;
MIDIPortRef port_in;
MIDIPortRef port_out;
struct InputConnection {
InputConnection() = default;
@ -61,6 +62,7 @@ class MIDIDriverCoreMidi : public MIDIDriver {
public:
virtual Error open() override;
virtual void close() override;
virtual Error send(Ref<InputEventMIDI> p_event) override;
MIDIDriverCoreMidi() = default;
virtual ~MIDIDriverCoreMidi();

View file

@ -77,6 +77,22 @@ Error MIDIDriverWinMidi::open() {
}
}
device_index = 0;
connected_sinks.resize(midiOutGetNumDevs());
MIDIOUTCAPS mic;
for (UINT i = 0; i < midiOutGetNumDevs(); i++) {
MMRESULT open_res = midiOutOpen(&connected_sinks.ptrw()[device_index], i, 0, (DWORD_PTR)device_index, CALLBACK_NULL);
printf("%d of %d %d\n", i, (int)midiOutGetNumDevs(), (int)open_res);
if (open_res == MMSYSERR_NOERROR) {
if (!midiOutGetDevCaps(i, &mic, sizeof(MIDIOUTCAPS))) {
connected_output_names.push_back(mic.szPname);
} else {
connected_output_names.push_back("ERROR");
}
device_index++;
}
}
return OK;
}
@ -86,8 +102,37 @@ void MIDIDriverWinMidi::close() {
midiInStop(midi_in);
midiInClose(midi_in);
}
for (int i = 0; i < connected_sinks.size(); i++) {
HMIDIOUT midi_out = connected_sinks[i];
midiOutReset(midi_out);
midiOutClose(midi_out);
}
connected_sources.clear();
connected_sinks.clear();
connected_input_names.clear();
connected_output_names.clear();
}
Error MIDIDriverWinMidi::send(Ref<InputEventMIDI> p_event) {
ERR_FAIL_COND_V(p_event.is_null(), ERR_INVALID_PARAMETER);
int device_id = p_event->get_device();
ERR_FAIL_INDEX_V(device_id, connected_sinks.size(), ERR_PARAMETER_RANGE_ERROR);
DWORD message = 0;
PackedByteArray packet = p_event->get_midi_bytes();
memcpy(&message, packet.ptr(), MIN(sizeof(message), size_t(packet.size())));
MMRESULT send_ok = midiOutShortMsg(connected_sinks[device_id], message);
switch (send_ok) {
case MMSYSERR_NOERROR:
return OK;
case MIDIERR_BADOPENMODE:
return ERR_UNCONFIGURED;
case MIDIERR_NOTREADY:
return ERR_BUSY;
case MMSYSERR_INVALHANDLE:
return ERR_DOES_NOT_EXIST;
default:
return FAILED;
}
}
MIDIDriverWinMidi::~MIDIDriverWinMidi() {

View file

@ -44,12 +44,14 @@
class MIDIDriverWinMidi : public MIDIDriver {
Vector<HMIDIIN> connected_sources;
Vector<HMIDIOUT> connected_sinks;
static void CALLBACK read(HMIDIIN hMidiIn, UINT wMsg, DWORD_PTR dwInstance, DWORD_PTR dwParam1, DWORD_PTR dwParam2);
public:
virtual Error open() override;
virtual void close() override;
virtual Error send(Ref<InputEventMIDI> p_event) override;
MIDIDriverWinMidi() = default;
virtual ~MIDIDriverWinMidi();