Skip to content

Commit

Permalink
Add webmidi support
Browse files Browse the repository at this point in the history
  • Loading branch information
ryanbraganza committed Nov 22, 2024
1 parent f952bfe commit d244d3f
Show file tree
Hide file tree
Showing 8 changed files with 300 additions and 3 deletions.
1 change: 1 addition & 0 deletions doc/classes/InputEventMIDI.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
8 changes: 5 additions & 3 deletions doc/classes/OS.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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">
Expand Down Expand Up @@ -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">
Expand Down Expand Up @@ -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">
Expand Down
2 changes: 2 additions & 0 deletions platform/web/SCsub
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
50 changes: 50 additions & 0 deletions platform/web/godot_midi.h
Original file line number Diff line number Diff line change
@@ -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
94 changes: 94 additions & 0 deletions platform/web/js/libs/library_godot_webmidi.js
Original file line number Diff line number Diff line change
@@ -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);
3 changes: 3 additions & 0 deletions platform/web/os_web.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
#define OS_WEB_H

#include "audio_driver_web.h"
#include "webmidi_driver.h"

#include "godot_js.h"

Expand All @@ -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;
Expand Down
86 changes: 86 additions & 0 deletions platform/web/webmidi_driver.cpp
Original file line number Diff line number Diff line change
@@ -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 <emscripten.h>

MIDIDriverWebMidi *MIDIDriverWebMidi::get_singleton() {
return static_cast<MIDIDriverWebMidi *>(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<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, 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);
}
59 changes: 59 additions & 0 deletions platform/web/webmidi_driver.h
Original file line number Diff line number Diff line change
@@ -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<String> &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

0 comments on commit d244d3f

Please sign in to comment.