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:
parent
92e51fca72
commit
df09a2afa4
15 changed files with 225 additions and 11 deletions
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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!");
|
||||
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -87,18 +87,28 @@ 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.
|
||||
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_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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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();
|
||||
|
|
Loading…
Reference in a new issue