Add Web MIDI support

This commit is contained in:
Ryan Braganza 2024-08-27 20:22:09 +10:00
parent 0c45ace151
commit 1ee098ddd7
8 changed files with 313 additions and 3 deletions

View File

@ -58,6 +58,7 @@
[/csharp]
[/codeblocks]
[b]Note:[/b] Godot does not support MIDI output, so there is no way to emit MIDI messages from Godot. Only MIDI input is supported.
[b]Note:[/b] On the Web platform, using MIDI input requires a browser permission to be granted first. This permission request is performed when calling [method OS.open_midi_inputs]. MIDI input will not work until the user accepts the permission request.
</description>
<tutorials>
<link title="MIDI Message Status Byte List">https://www.midi.org/specifications-old/item/table-2-expanded-messages-list-status-bytes</link>

View File

@ -23,7 +23,7 @@
<return type="void" />
<description>
Shuts down the system MIDI driver. Godot will no longer receive [InputEventMIDI]. See also [method open_midi_inputs] and [method get_connected_midi_inputs].
[b]Note:[/b] This method is implemented on Linux, macOS, and Windows.
[b]Note:[/b] This method is implemented on Linux, macOS, Web, and Windows.
</description>
</method>
<method name="crash">
@ -244,7 +244,8 @@
<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].
[b]Note:[/b] This method is implemented on Linux, macOS, and Windows.
[b]Note:[/b] This method is implemented on Linux, macOS, Web, and Windows.
[b]Note:[/b] On the Web platform, using MIDI input requires a browser permission to be granted first. This permission request is performed when calling [method open_midi_inputs]. The browser will refrain from processing MIDI input until the user accepts the permission request.
</description>
</method>
<method name="get_data_dir" qualifiers="const">
@ -698,7 +699,8 @@
<return type="void" />
<description>
Initializes the singleton for the system MIDI driver, allowing Godot to receive [InputEventMIDI]. See also [method get_connected_midi_inputs] and [method close_midi_inputs].
[b]Note:[/b] This method is implemented on Linux, macOS, and Windows.
[b]Note:[/b] This method is implemented on Linux, macOS, Web, and Windows.
[b]Note:[/b] On the Web platform, using MIDI input requires a browser permission to be granted first. This permission request is performed when calling [method open_midi_inputs]. The browser will refrain from processing MIDI input until the user accepts the permission request.
</description>
</method>
<method name="read_buffer_from_stdin">

View File

@ -22,6 +22,7 @@ if "serve" in COMMAND_LINE_TARGETS or "run" in COMMAND_LINE_TARGETS:
web_files = [
"audio_driver_web.cpp",
"webmidi_driver.cpp",
"display_server_web.cpp",
"http_client_web.cpp",
"javascript_bridge_singleton.cpp",
@ -38,6 +39,7 @@ sys_env.AddJSLibraries(
"js/libs/library_godot_audio.js",
"js/libs/library_godot_display.js",
"js/libs/library_godot_fetch.js",
"js/libs/library_godot_webmidi.js",
"js/libs/library_godot_os.js",
"js/libs/library_godot_runtime.js",
"js/libs/library_godot_input.js",

51
platform/web/godot_midi.h Normal file
View File

@ -0,0 +1,51 @@
/**************************************************************************/
/* godot_midi.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#ifndef GODOT_MIDI_H
#define GODOT_MIDI_H
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
extern int godot_js_webmidi_open_midi_inputs(
void (*p_callback)(int p_size, const char **p_connected_input_names),
void (*p_on_midi_message)(int p_device_index, int p_status, const uint8_t *p_data, int p_data_len),
const uint8_t *p_data_buffer,
const int p_data_buffer_len);
extern void godot_js_webmidi_close_midi_inputs();
#ifdef __cplusplus
}
#endif
#endif // GODOT_MIDI_H

View File

@ -0,0 +1,94 @@
/**************************************************************************/
/* library_godot_webmidi.js */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
const GodotWebMidi = {
$GodotWebMidi__deps: ['$GodotRuntime'],
$GodotWebMidi: {
abortControllers: [],
isListening: false,
},
godot_js_webmidi_open_midi_inputs__deps: ['$GodotWebMidi'],
godot_js_webmidi_open_midi_inputs__proxy: 'sync',
godot_js_webmidi_open_midi_inputs__sig: 'iiii',
godot_js_webmidi_open_midi_inputs: function (pSetInputNamesCb, pOnMidiMessageCb, pDataBuffer, dataBufferLen) {
if (GodotWebMidi.is_listening) {
return 0; // OK
}
if (!navigator.requestMIDIAccess) {
return 2; // ERR_UNAVAILABLE
}
const setInputNamesCb = GodotRuntime.get_func(pSetInputNamesCb);
const onMidiMessageCb = GodotRuntime.get_func(pOnMidiMessageCb);
GodotWebMidi.isListening = true;
navigator.requestMIDIAccess().then((midi) => {
const inputs = [...midi.inputs.values()];
const inputNames = inputs.map((input) => input.name);
const c_ptr = GodotRuntime.allocStringArray(inputNames);
setInputNamesCb(inputNames.length, c_ptr);
GodotRuntime.freeStringArray(c_ptr, inputNames.length);
inputs.forEach((input, i) => {
const abortController = new AbortController();
GodotWebMidi.abortControllers.push(abortController);
input.addEventListener('midimessage', (event) => {
const status = event.data[0];
const data = event.data.slice(1);
const size = data.length;
if (size > dataBufferLen) {
throw new Error(`data too big ${size} > ${dataBufferLen}`);
}
HEAPU8.set(data, pDataBuffer);
onMidiMessageCb(i, status, pDataBuffer, data.length);
}, { signal: abortController.signal });
});
});
return 0; // OK
},
godot_js_webmidi_close_midi_inputs__deps: ['$GodotWebMidi'],
godot_js_webmidi_close_midi_inputs__proxy: 'sync',
godot_js_webmidi_close_midi_inputs__sig: 'v',
godot_js_webmidi_close_midi_inputs: function () {
for (const abortController of GodotWebMidi.abortControllers) {
abortController.abort();
}
GodotWebMidi.abortControllers = [];
GodotWebMidi.isListening = false;
},
};
mergeInto(LibraryManager.library, GodotWebMidi);

View File

@ -32,6 +32,7 @@
#define OS_WEB_H
#include "audio_driver_web.h"
#include "webmidi_driver.h"
#include "godot_js.h"
@ -45,6 +46,8 @@ class OS_Web : public OS_Unix {
MainLoop *main_loop = nullptr;
List<AudioDriverWeb *> audio_drivers;
MIDIDriverWebMidi midi_driver;
bool idb_is_syncing = false;
bool idb_available = false;
bool idb_needs_sync = false;

View File

@ -0,0 +1,97 @@
/**************************************************************************/
/* webmidi_driver.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#include "webmidi_driver.h"
#ifdef PROXY_TO_PTHREAD_ENABLED
#include "core/object/callable_method_pointer.h"
#endif
MIDIDriverWebMidi *MIDIDriverWebMidi::get_singleton() {
return static_cast<MIDIDriverWebMidi *>(MIDIDriver::get_singleton());
}
Error MIDIDriverWebMidi::open() {
Error error = (Error)godot_js_webmidi_open_midi_inputs(&MIDIDriverWebMidi::set_input_names_callback, &MIDIDriverWebMidi::on_midi_message, _event_buffer, MIDIDriverWebMidi::MAX_EVENT_BUFFER_LENGTH);
if (error == ERR_UNAVAILABLE) {
ERR_PRINT("Web MIDI is not supported on this browser");
}
return error;
}
void MIDIDriverWebMidi::close() {
get_singleton()->connected_input_names.clear();
godot_js_webmidi_close_midi_inputs();
}
MIDIDriverWebMidi::~MIDIDriverWebMidi() {
close();
}
void MIDIDriverWebMidi::set_input_names_callback(int p_size, const char **p_input_names) {
Vector<String> input_names;
for (int i = 0; i < p_size; i++) {
input_names.append(String::utf8(p_input_names[i]));
}
#ifdef PROXY_TO_PTHREAD_ENABLED
if (!Thread::is_main_thread()) {
callable_mp_static(MIDIDriverWebMidi::_set_input_names_callback).call_deferred(input_names);
return;
}
#endif
_set_input_names_callback(input_names);
}
void MIDIDriverWebMidi::_set_input_names_callback(const Vector<String> &p_input_names) {
get_singleton()->connected_input_names.clear();
for (int i = 0; i < p_input_names.size(); i++) {
get_singleton()->connected_input_names.push_back(p_input_names[i]);
}
}
void MIDIDriverWebMidi::on_midi_message(int p_device_index, int p_status, const uint8_t *p_data, int p_data_len) {
PackedByteArray data;
data.resize(p_data_len);
uint8_t *data_ptr = data.ptrw();
for (int i = 0; i < p_data_len; i++) {
data_ptr[i] = p_data[i];
}
#ifdef PROXY_TO_PTHREAD_ENABLED
if (!Thread::is_main_thread()) {
callable_mp_static(MIDIDriverWebMidi::_on_midi_message).call_deferred(p_device_index, p_status, data, p_data_len);
return;
}
#endif
_on_midi_message(p_device_index, p_status, data, p_data_len);
}
void MIDIDriverWebMidi::_on_midi_message(int p_device_index, int p_status, const PackedByteArray &p_data, int p_data_len) {
MIDIDriver::send_event(p_device_index, p_status, p_data.ptr(), p_data_len);
}

View File

@ -0,0 +1,60 @@
/**************************************************************************/
/* webmidi_driver.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#ifndef WEBMIDI_DRIVER_H
#define WEBMIDI_DRIVER_H
#include "core/os/midi_driver.h"
#include "godot_js.h"
#include "godot_midi.h"
class MIDIDriverWebMidi : public MIDIDriver {
private:
static const int MAX_EVENT_BUFFER_LENGTH = 2;
uint8_t _event_buffer[MAX_EVENT_BUFFER_LENGTH];
public:
// Override return type to make writing static callbacks less tedious.
static MIDIDriverWebMidi *get_singleton();
virtual Error open() override;
virtual void close() override final;
MIDIDriverWebMidi() = default;
virtual ~MIDIDriverWebMidi();
WASM_EXPORT static void set_input_names_callback(int p_size, const char **p_input_names);
static void _set_input_names_callback(const Vector<String> &p_input_names);
WASM_EXPORT static void on_midi_message(int p_device_index, int p_status, const uint8_t *p_data, int p_data_len);
static void _on_midi_message(int p_device_index, int p_status, const PackedByteArray &p_data, int p_data_len);
};
#endif // WEBMIDI_DRIVER_H