From d244d3f7220832dff5f3b3797f800f7af0e2af0e Mon Sep 17 00:00:00 2001 From: Ryan Braganza Date: Tue, 27 Aug 2024 20:22:09 +1000 Subject: [PATCH] Add webmidi support --- doc/classes/InputEventMIDI.xml | 1 + doc/classes/OS.xml | 8 +- platform/web/SCsub | 2 + platform/web/godot_midi.h | 50 ++++++++++ platform/web/js/libs/library_godot_webmidi.js | 94 +++++++++++++++++++ platform/web/os_web.h | 3 + platform/web/webmidi_driver.cpp | 86 +++++++++++++++++ platform/web/webmidi_driver.h | 59 ++++++++++++ 8 files changed, 300 insertions(+), 3 deletions(-) create mode 100644 platform/web/godot_midi.h create mode 100644 platform/web/js/libs/library_godot_webmidi.js create mode 100644 platform/web/webmidi_driver.cpp create mode 100644 platform/web/webmidi_driver.h diff --git a/doc/classes/InputEventMIDI.xml b/doc/classes/InputEventMIDI.xml index 4dcaf987473e..c3b49408c20c 100644 --- a/doc/classes/InputEventMIDI.xml +++ b/doc/classes/InputEventMIDI.xml @@ -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. https://www.midi.org/specifications-old/item/table-2-expanded-messages-list-status-bytes diff --git a/doc/classes/OS.xml b/doc/classes/OS.xml index 5ab7c27f4f4c..e2bd99f8482c 100644 --- a/doc/classes/OS.xml +++ b/doc/classes/OS.xml @@ -23,7 +23,7 @@ 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. @@ -244,7 +244,8 @@ 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. @@ -698,7 +699,8 @@ 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. diff --git a/platform/web/SCsub b/platform/web/SCsub index a85fbcd0f576..37668a72f7df 100644 --- a/platform/web/SCsub +++ b/platform/web/SCsub @@ -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", diff --git a/platform/web/godot_midi.h b/platform/web/godot_midi.h new file mode 100644 index 000000000000..33dec0d5e0ef --- /dev/null +++ b/platform/web/godot_midi.h @@ -0,0 +1,50 @@ +/**************************************************************************/ +/* 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 + +extern Error 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, uint8_t p_status, const uint8_t *p_data, size_t p_data_len), + const uint8_t *p_data_buffer, + const size_t p_data_buffer_len); + +extern void godot_js_webmidi_close_midi_inputs(); + +#ifdef __cplusplus +} +#endif + +#endif // GODOT_MIDI_H diff --git a/platform/web/js/libs/library_godot_webmidi.js b/platform/web/js/libs/library_godot_webmidi.js new file mode 100644 index 000000000000..1c94b1559fcd --- /dev/null +++ b/platform/web/js/libs/library_godot_webmidi.js @@ -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); diff --git a/platform/web/os_web.h b/platform/web/os_web.h index 1ddb745965ac..e9040f04d4d3 100644 --- a/platform/web/os_web.h +++ b/platform/web/os_web.h @@ -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 audio_drivers; + MIDIDriverWebMidi midi_driver; + bool idb_is_syncing = false; bool idb_available = false; bool idb_needs_sync = false; diff --git a/platform/web/webmidi_driver.cpp b/platform/web/webmidi_driver.cpp new file mode 100644 index 000000000000..b990a91b28c0 --- /dev/null +++ b/platform/web/webmidi_driver.cpp @@ -0,0 +1,86 @@ +/**************************************************************************/ +/* 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" + +#include + +MIDIDriverWebMidi *MIDIDriverWebMidi::get_singleton() { + return static_cast(MIDIDriver::get_singleton()); +} + +Error MIDIDriverWebMidi::open() { + 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 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 &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, uint8_t p_status, const uint8_t *p_data, size_t p_data_len) { +#ifdef PROXY_TO_PTHREAD_ENABLED + if (!Thread::is_main_thread()) { + callable_mp_static(MIDIDriverWebMidi::send_event).call_deferred(p_device_index, p_status, p_data, p_data_len); + return; + } +#endif + MIDIDriver::send_event(p_device_index, p_status, p_data, p_data_len); +} diff --git a/platform/web/webmidi_driver.h b/platform/web/webmidi_driver.h new file mode 100644 index 000000000000..c57ddd0ca2e7 --- /dev/null +++ b/platform/web/webmidi_driver.h @@ -0,0 +1,59 @@ +/**************************************************************************/ +/* 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 &p_input_names); + + WASM_EXPORT static void on_midi_message(int p_device_index, uint8_t p_status, const uint8_t *p_data, size_t p_data_len); +}; + +#endif // WEBMIDI_DRIVER_H