├── .gitignore ├── LICENSE ├── README.md ├── midi-input-example ├── CheckBox.gd ├── Root.gd ├── Root.tscn ├── default_env.tres ├── icon.png ├── icon.png.import └── project.godot ├── rancidbacon-godot-midi-input-example-screencap.gif └── rancidbacon-godot-midi-input-example-screenshot.png /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Godot-specific ignores 3 | .import/ 4 | export.cfg 5 | export_presets.cfg 6 | 7 | # Mono-specific ignores 8 | .mono/ 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 follower@rancidbacon.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Godot MIDI Input example 2 | 3 | **Home:** 4 | 5 | **Official Godot docs:** 6 | 7 | * [`InputEventMIDI`](https://docs.godotengine.org/en/latest/classes/class_inputeventmidi.html), [`MidiMessageList`](https://docs.godotengine.org/en/latest/classes/class_@globalscope.html#enum-globalscope-midimessagelist) 8 | 9 | * [`open_midi_inputs()`](https://docs.godotengine.org/en/latest/classes/class_os.html#class-os-method-open-midi-inputs), [`get_connected_midi_inputs()`](https://docs.godotengine.org/en/latest/classes/class_os.html#class-os-method-get-connected-midi-inputs), [`close_midi_inputs()`](https://docs.godotengine.org/en/latest/classes/class_os.html#class-os-method-close-midi-inputs) 10 | 11 | ![](rancidbacon-godot-midi-input-example-screenshot.png) 12 | 13 | ## The Demo 14 | 15 | The enclosed Godot 3.1-compatible project lists available MIDI input devices, dumps textual information about the events received and visualises key presses on a 1-octave on screen keyboard. 16 | 17 | ## Just The Code... 18 | 19 | If you just want some example code so you can implement your own MIDI Input-based Godot project... 20 | 21 | ```gdscript 22 | 23 | # The MIDI Input specific parts... 24 | 25 | func _ready(): 26 | OS.open_midi_inputs() # Required for cross-platform reliability. 27 | 28 | print(OS.get_connected_midi_inputs()) # List available MIDI input sources (e.g. keyboard, controller). 29 | 30 | 31 | func _unhandled_input(event : InputEvent): 32 | 33 | if (event is InputEventMIDI): # When we get a MIDI input event... 34 | 35 | var event_dump_rb : String = "" 36 | 37 | # Display the available property values... 38 | event_dump_rb += "chn: {channel} msg: {message}\n".format({"channel": event.channel, "message": event.message}) 39 | event_dump_rb += " pitch: {pitch} vel: {velocity}\n".format({"pitch": event.pitch, "velocity": event.velocity}) 40 | 41 | event_dump_rb += "\n" 42 | 43 | print(event_dump_rb) 44 | 45 | 46 | # Example of converting pitch to a keyboard key (not a musical key) within an octave. 47 | var key_index = event.pitch % 12 48 | 49 | 50 | # Handle the received message type appropriately... 51 | match event.message: 52 | MIDI_MESSAGE_NOTE_ON: 53 | # Do something here... 54 | 55 | MIDI_MESSAGE_NOTE_OFF: 56 | # Do something here... 57 | 58 | ``` 59 | 60 | *Brought to you by follower at .* 61 | -------------------------------------------------------------------------------- /midi-input-example/CheckBox.gd: -------------------------------------------------------------------------------- 1 | extends CheckBox 2 | 3 | 4 | 5 | func _on_CheckBox_toggled(_button_pressed): 6 | $"../RichTextLabel".scroll_following = !$"../RichTextLabel".scroll_following 7 | -------------------------------------------------------------------------------- /midi-input-example/Root.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | # Called when the node enters the scene tree for the first time. 4 | func _ready(): 5 | OS.open_midi_inputs() 6 | 7 | print(OS.get_connected_midi_inputs()) 8 | 9 | for current_midi_input in OS.get_connected_midi_inputs(): 10 | print(current_midi_input) 11 | $"Control/VBoxContainer/MidiInputsList".add_item(current_midi_input, null, false) 12 | 13 | 14 | # via 15 | enum GlobalScope_MidiMessageList { 16 | MIDI_MESSAGE_NOTE_OFF = 0x8, 17 | MIDI_MESSAGE_NOTE_ON = 0x9, 18 | MIDI_MESSAGE_AFTERTOUCH = 0xA, 19 | MIDI_MESSAGE_CONTROL_CHANGE = 0xB, 20 | MIDI_MESSAGE_PROGRAM_CHANGE = 0xC, 21 | MIDI_MESSAGE_CHANNEL_PRESSURE = 0xD, 22 | MIDI_MESSAGE_PITCH_BEND = 0xE, 23 | }; 24 | 25 | 26 | const MIDI_EVENT_PROPERTIES = ["channel", "message", "pitch", "velocity", "instrument", "pressure", "controller_number", "controller_value"] 27 | 28 | 29 | func get_midi_message_description(event : InputEventMIDI): 30 | 31 | if GlobalScope_MidiMessageList.values().has(event.message): 32 | return GlobalScope_MidiMessageList.keys()[event.message - 0x08] 33 | return event.message 34 | 35 | # TODO: Add the black keys. 36 | const OCTAVE_KEY_INDEX = ["WhiteKey1", "BlackKey1", "WhiteKey2", "BlackKey2", "WhiteKey3", "WhiteKey4", "BlackKey3", "WhiteKey5", "BlackKey4", "WhiteKey6", "BlackKey5", "WhiteKey7"] 37 | 38 | 39 | func _unhandled_input(event : InputEvent): 40 | 41 | if (event is InputEventMIDI): 42 | 43 | var event_dump : String = "" 44 | 45 | #event_dump += "chn: {channel} msg: {message}\n".format({"channel": event.channel, "message": event.message}) 46 | #event_dump += " pitch: {pitch} vel: {velocity}\n".format({"pitch": event.pitch, "velocity": event.velocity}) 47 | 48 | event_dump += "event: {0}\n".format([get_midi_message_description(event)]) 49 | 50 | for current_property in MIDI_EVENT_PROPERTIES: 51 | event_dump += " {0}: {1}\n".format([current_property, event.get(current_property)]) 52 | 53 | event_dump += "\n" 54 | 55 | # NOTE: Having "Scroll Following" enabled seems to break things if there are 56 | # too many messages sent at once. 57 | $"Control/VBoxContainer/Container/VBoxContainer/RichTextLabel".add_text(event_dump) 58 | 59 | 60 | # Note: If the instrument isn't keyboard related it will possibly mess up the display. 61 | var key_index = event.pitch % 12 62 | 63 | match event.message: 64 | MIDI_MESSAGE_NOTE_ON: 65 | var current_key_node : CSGPrimitive = get_node("3DRoot/Octave/" + OCTAVE_KEY_INDEX[key_index]) 66 | current_key_node.translate(Vector3(0, -0.125, 0)) 67 | 68 | MIDI_MESSAGE_NOTE_OFF: 69 | var current_key_node : CSGPrimitive = get_node("3DRoot/Octave/" + OCTAVE_KEY_INDEX[key_index]) 70 | current_key_node.translate(Vector3(0, +0.125, 0)) 71 | -------------------------------------------------------------------------------- /midi-input-example/Root.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=7 format=2] 2 | 3 | [ext_resource path="res://Root.gd" type="Script" id=1] 4 | [ext_resource path="res://CheckBox.gd" type="Script" id=2] 5 | 6 | [sub_resource type="VisualShaderNodeColorUniform" id=1] 7 | output_port_for_preview = 0 8 | uniform_name = "ColorUniform" 9 | 10 | [sub_resource type="VisualShader" id=2] 11 | code = "shader_type spatial; 12 | uniform vec4 ColorUniform : hint_color; 13 | 14 | 15 | 16 | void vertex() { 17 | // Output:0 18 | 19 | } 20 | 21 | void fragment() { 22 | // ColorUniform:2 23 | vec3 n_out2p0; 24 | float n_out2p1; 25 | n_out2p0 = ColorUniform.rgb; 26 | n_out2p1 = ColorUniform.a; 27 | 28 | // Output:0 29 | ALBEDO = n_out2p0; 30 | 31 | } 32 | 33 | void light() { 34 | // Output:0 35 | 36 | } 37 | " 38 | graph_offset = Vector2( 0, -8.775 ) 39 | nodes/fragment/0/position = Vector2( 600, 20 ) 40 | nodes/fragment/2/node = SubResource( 1 ) 41 | nodes/fragment/2/position = Vector2( 160, 80 ) 42 | nodes/fragment/connections = PoolIntArray( 2, 0, 0, 0 ) 43 | 44 | [sub_resource type="ShaderMaterial" id=3] 45 | shader = SubResource( 2 ) 46 | shader_param/ColorUniform = null 47 | 48 | [sub_resource type="StyleBoxFlat" id=4] 49 | bg_color = Color( 0.6, 0.6, 0.6, 0.25098 ) 50 | border_width_left = 4 51 | border_width_top = 4 52 | border_width_right = 4 53 | border_width_bottom = 4 54 | border_color = Color( 0.8, 0.8, 0.8, 0 ) 55 | expand_margin_left = 2.0 56 | expand_margin_right = 2.0 57 | expand_margin_top = 2.0 58 | expand_margin_bottom = 2.0 59 | 60 | [node name="Root" type="Node"] 61 | script = ExtResource( 1 ) 62 | 63 | [node name="3DRoot" type="Spatial" parent="."] 64 | 65 | [node name="Camera" type="Camera" parent="3DRoot"] 66 | transform = Transform( 1, 0, 0, 0, 0.906308, 0.422618, 0, -0.422618, 0.906308, 0, 2, 4 ) 67 | 68 | [node name="Octave" type="CSGCombiner" parent="3DRoot"] 69 | transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, -3, 0, 0 ) 70 | 71 | [node name="WhiteKey1" type="CSGBox" parent="3DRoot/Octave"] 72 | transform = Transform( 0.95, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0 ) 73 | width = 1.0 74 | height = 0.5 75 | 76 | [node name="BlackKey1" type="CSGBox" parent="3DRoot/Octave"] 77 | transform = Transform( 0.75, 0, 0, 0, 1, 0, 0, 0, 0.65, 0.5, 0.25, -0.35 ) 78 | width = 1.0 79 | height = 0.5 80 | material = SubResource( 3 ) 81 | 82 | [node name="WhiteKey2" type="CSGBox" parent="3DRoot/Octave"] 83 | transform = Transform( 0.95, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0 ) 84 | width = 1.0 85 | height = 0.5 86 | 87 | [node name="BlackKey2" type="CSGBox" parent="3DRoot/Octave"] 88 | transform = Transform( 0.75, 0, 0, 0, 1, 0, 0, 0, 0.65, 1.5, 0.25, -0.35 ) 89 | width = 1.0 90 | height = 0.5 91 | material = SubResource( 3 ) 92 | 93 | [node name="WhiteKey3" type="CSGBox" parent="3DRoot/Octave"] 94 | transform = Transform( 0.95, 0, 0, 0, 1, 0, 0, 0, 1, 2, 0, 0 ) 95 | width = 1.0 96 | height = 0.5 97 | 98 | [node name="WhiteKey4" type="CSGBox" parent="3DRoot/Octave"] 99 | transform = Transform( 0.95, 0, 0, 0, 1, 0, 0, 0, 1, 3, 0, 0 ) 100 | width = 1.0 101 | height = 0.5 102 | 103 | [node name="BlackKey3" type="CSGBox" parent="3DRoot/Octave"] 104 | transform = Transform( 0.75, 0, 0, 0, 1, 0, 0, 0, 0.65, 3.5, 0.25, -0.35 ) 105 | width = 1.0 106 | height = 0.5 107 | material = SubResource( 3 ) 108 | 109 | [node name="WhiteKey5" type="CSGBox" parent="3DRoot/Octave"] 110 | transform = Transform( 0.95, 0, 0, 0, 1, 0, 0, 0, 1, 4, 0, 0 ) 111 | width = 1.0 112 | height = 0.5 113 | 114 | [node name="BlackKey4" type="CSGBox" parent="3DRoot/Octave"] 115 | transform = Transform( 0.75, 0, 0, 0, 1, 0, 0, 0, 0.65, 4.5, 0.25, -0.35 ) 116 | width = 1.0 117 | height = 0.5 118 | material = SubResource( 3 ) 119 | 120 | [node name="WhiteKey6" type="CSGBox" parent="3DRoot/Octave"] 121 | transform = Transform( 0.95, 0, 0, 0, 1, 0, 0, 0, 1, 5, 0, 0 ) 122 | width = 1.0 123 | height = 0.5 124 | 125 | [node name="BlackKey5" type="CSGBox" parent="3DRoot/Octave"] 126 | transform = Transform( 0.75, 0, 0, 0, 1, 0, 0, 0, 0.65, 5.5, 0.25, -0.35 ) 127 | width = 1.0 128 | height = 0.5 129 | material = SubResource( 3 ) 130 | 131 | [node name="WhiteKey7" type="CSGBox" parent="3DRoot/Octave"] 132 | transform = Transform( 0.95, 0, 0, 0, 1, 0, 0, 0, 1, 6, 0, 0 ) 133 | width = 1.0 134 | height = 0.5 135 | 136 | [node name="Control" type="Control" parent="."] 137 | margin_right = 1020.0 138 | margin_bottom = 600.0 139 | 140 | [node name="VBoxContainer" type="VBoxContainer" parent="Control"] 141 | margin_left = 4.0 142 | margin_top = 10.0 143 | margin_right = 1020.0 144 | margin_bottom = 590.0 145 | 146 | [node name="Label" type="Label" parent="Control/VBoxContainer"] 147 | margin_right = 1016.0 148 | margin_bottom = 14.0 149 | custom_colors/font_color = Color( 0, 0, 0, 1 ) 150 | custom_colors/font_outline_modulate = Color( 0, 0, 0, 1 ) 151 | text = "Godot MIDI Input Example" 152 | align = 1 153 | valign = 1 154 | 155 | [node name="Container" type="HSplitContainer" parent="Control/VBoxContainer"] 156 | margin_top = 18.0 157 | margin_right = 1016.0 158 | margin_bottom = 567.0 159 | size_flags_vertical = 3 160 | 161 | [node name="Container" type="Container" parent="Control/VBoxContainer/Container"] 162 | margin_right = 671.0 163 | margin_bottom = 549.0 164 | size_flags_horizontal = 3 165 | size_flags_stretch_ratio = 2.0 166 | 167 | [node name="VBoxContainer" type="VBoxContainer" parent="Control/VBoxContainer/Container"] 168 | margin_left = 683.0 169 | margin_right = 1016.0 170 | margin_bottom = 549.0 171 | size_flags_horizontal = 3 172 | size_flags_vertical = 3 173 | 174 | [node name="RichTextLabel" type="RichTextLabel" parent="Control/VBoxContainer/Container/VBoxContainer"] 175 | margin_right = 333.0 176 | margin_bottom = 521.0 177 | size_flags_horizontal = 3 178 | size_flags_vertical = 3 179 | custom_styles/normal = SubResource( 4 ) 180 | custom_colors/default_color = Color( 0, 0, 0, 1 ) 181 | scroll_following = true 182 | 183 | [node name="CheckBox" type="CheckBox" parent="Control/VBoxContainer/Container/VBoxContainer"] 184 | margin_top = 525.0 185 | margin_right = 333.0 186 | margin_bottom = 549.0 187 | hint_tooltip = "Some MIDI devices send messages faster than the scroll area can handle. If this happens disable \"Auto scroll output\". " 188 | pressed = true 189 | text = "Auto scroll output" 190 | script = ExtResource( 2 ) 191 | 192 | [node name="MidiInputsList" type="ItemList" parent="Control/VBoxContainer"] 193 | margin_top = 571.0 194 | margin_right = 1016.0 195 | margin_bottom = 580.0 196 | auto_height = true 197 | 198 | [connection signal="toggled" from="Control/VBoxContainer/Container/VBoxContainer/CheckBox" to="Control/VBoxContainer/Container/VBoxContainer/CheckBox" method="_on_CheckBox_toggled"] 199 | -------------------------------------------------------------------------------- /midi-input-example/default_env.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="Environment" load_steps=2 format=2] 2 | 3 | [sub_resource type="ProceduralSky" id=1] 4 | 5 | [resource] 6 | background_mode = 2 7 | background_sky = SubResource( 1 ) 8 | 9 | -------------------------------------------------------------------------------- /midi-input-example/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/follower/godot-midi-input-example/742866c0ba156d215e70bb99f5c5b702ecc89891/midi-input-example/icon.png -------------------------------------------------------------------------------- /midi-input-example/icon.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="StreamTexture" 5 | path="res://.import/icon.png-487276ed1e3a0c39cad0279d744ee560.stex" 6 | metadata={ 7 | "vram_texture": false 8 | } 9 | 10 | [deps] 11 | 12 | source_file="res://icon.png" 13 | dest_files=[ "res://.import/icon.png-487276ed1e3a0c39cad0279d744ee560.stex" ] 14 | 15 | [params] 16 | 17 | compress/mode=0 18 | compress/lossy_quality=0.7 19 | compress/hdr_mode=0 20 | compress/bptc_ldr=0 21 | compress/normal_map=0 22 | flags/repeat=0 23 | flags/filter=true 24 | flags/mipmaps=false 25 | flags/anisotropic=false 26 | flags/srgb=2 27 | process/fix_alpha_border=true 28 | process/premult_alpha=false 29 | process/HDR_as_SRGB=false 30 | process/invert_color=false 31 | stream=false 32 | size_limit=0 33 | detect_3d=true 34 | svg/scale=1.0 35 | -------------------------------------------------------------------------------- /midi-input-example/project.godot: -------------------------------------------------------------------------------- 1 | ; Engine configuration file. 2 | ; It's best edited using the editor UI and not directly, 3 | ; since the parameters that go here are not all obvious. 4 | ; 5 | ; Format: 6 | ; [section] ; section goes between [] 7 | ; param=value ; assign values to parameters 8 | 9 | config_version=4 10 | 11 | _global_script_classes=[ ] 12 | _global_script_class_icons={ 13 | 14 | } 15 | 16 | [application] 17 | 18 | config/name="midi-input-example" 19 | run/main_scene="res://Root.tscn" 20 | config/icon="res://icon.png" 21 | 22 | [rendering] 23 | 24 | quality/driver/driver_name="GLES2" 25 | environment/default_environment="res://default_env.tres" 26 | -------------------------------------------------------------------------------- /rancidbacon-godot-midi-input-example-screencap.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/follower/godot-midi-input-example/742866c0ba156d215e70bb99f5c5b702ecc89891/rancidbacon-godot-midi-input-example-screencap.gif -------------------------------------------------------------------------------- /rancidbacon-godot-midi-input-example-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/follower/godot-midi-input-example/742866c0ba156d215e70bb99f5c5b702ecc89891/rancidbacon-godot-midi-input-example-screenshot.png --------------------------------------------------------------------------------