Browse Source

Add MIDI Piano Demo

Aaron Franke 3 years ago
parent
commit
be43d1644b

+ 19 - 0
audio/midi_piano/README.md

@@ -0,0 +1,19 @@
+# MIDI Piano
+
+This demo shows how to use
+[InputEventMIDI](https://docs.godotengine.org/en/latest/classes/class_inputeventmidi.html)
+by creating a piano that can be controlled by a MIDI device.
+This is known to work with a Yamaha MX88.
+
+The piano can also be controlled by clicking on the keys, or by
+manually calling the activate and deactivate methods on each key.
+
+Note that MIDI output is not yet supported in Godot, only input works.
+
+Language: GDScript
+
+Renderer: GLES 2
+
+## Screenshots
+
+![Screenshot](screenshots/piano-pressed.png)

BIN
audio/midi_piano/icon.png


+ 35 - 0
audio/midi_piano/icon.png.import

@@ -0,0 +1,35 @@
+[remap]
+
+importer="texture"
+type="StreamTexture"
+path="res://.import/icon.png-487276ed1e3a0c39cad0279d744ee560.stex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://icon.png"
+dest_files=[ "res://.import/icon.png-487276ed1e3a0c39cad0279d744ee560.stex" ]
+
+[params]
+
+compress/mode=0
+compress/lossy_quality=0.7
+compress/hdr_mode=0
+compress/bptc_ldr=0
+compress/normal_map=0
+flags/repeat=0
+flags/filter=true
+flags/mipmaps=false
+flags/anisotropic=false
+flags/srgb=2
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/HDR_as_SRGB=false
+process/invert_color=false
+process/normal_map_invert_y=false
+stream=false
+size_limit=0
+detect_3d=true
+svg/scale=1.0

+ 97 - 0
audio/midi_piano/piano.gd

@@ -0,0 +1,97 @@
+extends Control
+
+# A standard piano with 88 keys has keys from 21 to 108.
+# To get a different set of keys, modify these numbers.
+# A maximally extended 108-key piano goes from 12 to 119.
+# A 76-key piano goes from 23 to 98, 61-key from 36 to 96,
+# 49-key from 36 to 84, 37-key from 41 to 77, and 25-key
+# from 48 to 72. Middle C is pitch number 60, A440 is 69.
+const START_KEY = 21
+const END_KEY = 108
+
+const WhiteKeyScene = preload("res://piano_keys/white_piano_key.tscn")
+const BlackKeyScene = preload("res://piano_keys/black_piano_key.tscn")
+
+var piano_key_dict := Dictionary()
+
+onready var white_keys = $WhiteKeys
+onready var black_keys = $BlackKeys
+
+func _ready():
+	# Sanity checks.
+	if _is_note_index_sharp(_pitch_index_to_note_index(START_KEY)):
+		printerr("The start key can't be a sharp note (limitation of this piano-generating algorithm). Try 21.")
+		return
+
+	for i in range(START_KEY, END_KEY + 1):
+		piano_key_dict[i] = _create_piano_key(i)
+
+	if white_keys.get_child_count() != black_keys.get_child_count():
+		_add_placeholder_key(black_keys)
+	OS.open_midi_inputs()
+	print(OS.get_connected_midi_inputs())
+
+
+func _input(input_event):
+	if not (input_event is InputEventMIDI):
+		return
+	var midi_event: InputEventMIDI = input_event
+	if midi_event.pitch < START_KEY or midi_event.pitch > END_KEY:
+		# The given pitch isn't on the on-screen keyboard, so return.
+		return
+	_print_midi_info(midi_event)
+	var key: PianoKey = piano_key_dict[midi_event.pitch]
+	if midi_event.message == MIDI_MESSAGE_NOTE_ON:
+		key.activate()
+	else:
+		key.deactivate()
+
+
+func _add_placeholder_key(container):
+	var placeholder = Control.new()
+	placeholder.size_flags_horizontal = SIZE_EXPAND_FILL
+	placeholder.mouse_filter = Control.MOUSE_FILTER_IGNORE
+	placeholder.name = "Placeholder"
+	container.add_child(placeholder)
+
+
+func _create_piano_key(pitch_index):
+	var note_index = _pitch_index_to_note_index(pitch_index)
+	var piano_key: PianoKey
+	if _is_note_index_sharp(note_index):
+		piano_key = BlackKeyScene.instance()
+		black_keys.add_child(piano_key)
+	else:
+		piano_key = WhiteKeyScene.instance()
+		white_keys.add_child(piano_key)
+		if _is_note_index_lacking_sharp(note_index):
+			_add_placeholder_key(black_keys)
+	piano_key.setup(pitch_index)
+	return piano_key
+
+
+func _is_note_index_lacking_sharp(note_index: int):
+	# B and E, because no B# or E#
+	return note_index in [2, 7]
+
+
+func _is_note_index_sharp(note_index: int):
+	# A#, C#, D#, F#, and G#
+	return note_index in [1, 4, 6, 9, 11]
+
+
+func _pitch_index_to_note_index(pitch: int):
+	pitch += 3
+	return pitch % 12
+
+
+func _print_midi_info(midi_event: InputEventMIDI):
+	print(midi_event)
+	print("Channel: " + str(midi_event.channel))
+	print("Message: " + str(midi_event.message))
+	print("Pitch: " + str(midi_event.pitch))
+	print("Velocity: " + str(midi_event.velocity))
+	print("Instrument: " + str(midi_event.instrument))
+	print("Pressure: " + str(midi_event.pressure))
+	print("Controller number: " + str(midi_event.controller_number))
+	print("Controller value: " + str(midi_event.controller_value))

+ 33 - 0
audio/midi_piano/piano.tscn

@@ -0,0 +1,33 @@
+[gd_scene load_steps=2 format=2]
+
+[ext_resource path="res://piano.gd" type="Script" id=1]
+
+[node name="Piano" type="ColorRect"]
+anchor_right = 1.0
+anchor_bottom = 1.0
+rect_min_size = Vector2( 200, 20 )
+color = Color( 0, 0, 0, 1 )
+script = ExtResource( 1 )
+__meta__ = {
+"_edit_use_anchors_": false
+}
+
+[node name="WhiteKeys" type="HBoxContainer" parent="."]
+anchor_right = 1.0
+anchor_bottom = 1.0
+rect_min_size = Vector2( 200, 0 )
+mouse_filter = 2
+custom_constants/separation = 0
+__meta__ = {
+"_edit_use_anchors_": false
+}
+
+[node name="BlackKeys" type="HBoxContainer" parent="."]
+anchor_right = 1.0
+anchor_bottom = 0.6
+rect_min_size = Vector2( 100, 0 )
+mouse_filter = 2
+custom_constants/separation = 0
+__meta__ = {
+"_edit_use_anchors_": false
+}

BIN
audio/midi_piano/piano_keys/A440.wav


+ 21 - 0
audio/midi_piano/piano_keys/A440.wav.import

@@ -0,0 +1,21 @@
+[remap]
+
+importer="wav"
+type="AudioStreamSample"
+path="res://.import/A440.wav-f6ecb7d5a329df710ac81894c403bbb6.sample"
+
+[deps]
+
+source_file="res://piano_keys/A440.wav"
+dest_files=[ "res://.import/A440.wav-f6ecb7d5a329df710ac81894c403bbb6.sample" ]
+
+[params]
+
+force/8_bit=false
+force/mono=false
+force/max_rate=false
+force/max_rate_hz=44100
+edit/trim=false
+edit/normalize=false
+edit/loop=false
+compress/mode=0

+ 32 - 0
audio/midi_piano/piano_keys/black_piano_key.tscn

@@ -0,0 +1,32 @@
+[gd_scene load_steps=3 format=2]
+
+[ext_resource path="res://piano_keys/piano_key.gd" type="Script" id=1]
+[ext_resource path="res://piano_keys/piano_key_color.gd" type="Script" id=2]
+
+[node name="PianoKey" type="Control"]
+margin_right = 20.0
+margin_bottom = 200.0
+mouse_filter = 2
+size_flags_horizontal = 3
+script = ExtResource( 1 )
+__meta__ = {
+"_edit_use_anchors_": false
+}
+
+[node name="Key" type="ColorRect" parent="."]
+anchor_left = 0.5
+anchor_right = 1.5
+anchor_bottom = 1.0
+margin_left = 2.0
+margin_right = -2.0
+size_flags_horizontal = 3
+color = Color( 0, 0, 0, 1 )
+script = ExtResource( 2 )
+__meta__ = {
+"_edit_use_anchors_": false
+}
+
+[node name="ColorTimer" type="Timer" parent="."]
+one_shot = true
+
+[connection signal="timeout" from="ColorTimer" to="." method="deactivate"]

+ 30 - 0
audio/midi_piano/piano_keys/piano_key.gd

@@ -0,0 +1,30 @@
+class_name PianoKey
+extends Control
+
+var pitch_scale: float
+
+onready var key: ColorRect = $Key
+onready var start_color: Color = key.color
+onready var color_timer: Timer = $ColorTimer
+
+
+func setup(pitch_index: int):
+	name = "PianoKey" + str(pitch_index)
+	var exponent := (pitch_index - 69.0) / 12.0
+	pitch_scale = pow(2, exponent)
+
+
+func activate():
+	key.color = (Color.yellow + start_color) / 2
+	var audio := AudioStreamPlayer.new()
+	add_child(audio)
+	audio.stream = preload("res://piano_keys/A440.wav")
+	audio.pitch_scale = pitch_scale
+	audio.play()
+	color_timer.start()
+	yield(get_tree().create_timer(8.0), "timeout")
+	audio.queue_free()
+
+
+func deactivate():
+	key.color = start_color

+ 8 - 0
audio/midi_piano/piano_keys/piano_key_color.gd

@@ -0,0 +1,8 @@
+extends ColorRect
+
+onready var parent = get_parent()
+
+# Yes, this script exists just for this one method.
+func _gui_input(input_event):
+	if input_event is InputEventMouseButton and input_event.pressed:
+		parent.activate()

+ 29 - 0
audio/midi_piano/piano_keys/white_piano_key.tscn

@@ -0,0 +1,29 @@
+[gd_scene load_steps=3 format=2]
+
+[ext_resource path="res://piano_keys/piano_key.gd" type="Script" id=1]
+[ext_resource path="res://piano_keys/piano_key_color.gd" type="Script" id=2]
+
+[node name="PianoKey" type="Control"]
+margin_right = 20.0
+margin_bottom = 200.0
+size_flags_horizontal = 3
+script = ExtResource( 1 )
+__meta__ = {
+"_edit_use_anchors_": false
+}
+
+[node name="Key" type="ColorRect" parent="."]
+anchor_right = 1.0
+anchor_bottom = 1.0
+margin_left = 1.0
+margin_right = -1.0
+size_flags_horizontal = 3
+script = ExtResource( 2 )
+__meta__ = {
+"_edit_use_anchors_": false
+}
+
+[node name="ColorTimer" type="Timer" parent="."]
+one_shot = true
+
+[connection signal="timeout" from="ColorTimer" to="." method="deactivate"]

+ 44 - 0
audio/midi_piano/project.godot

@@ -0,0 +1,44 @@
+; Engine configuration file.
+; It's best edited using the editor UI and not directly,
+; since the parameters that go here are not all obvious.
+;
+; Format:
+;   [section] ; section goes between []
+;   param=value ; assign values to parameters
+
+config_version=4
+
+_global_script_classes=[ {
+"base": "Control",
+"class": "PianoKey",
+"language": "GDScript",
+"path": "res://piano_keys/piano_key.gd"
+} ]
+_global_script_class_icons={
+"PianoKey": ""
+}
+
+[application]
+
+config/name="MIDI Piano Demo"
+run/main_scene="res://piano.tscn"
+config/icon="res://icon.png"
+
+[display]
+
+window/size/width=1280
+window/size/height=200
+
+[network]
+
+limits/debugger_stdout/max_chars_per_second=2048000
+
+[physics]
+
+common/enable_pause_aware_picking=true
+
+[rendering]
+
+quality/driver/driver_name="GLES2"
+vram_compression/import_etc=true
+vram_compression/import_etc2=false

+ 0 - 0
audio/midi_piano/screenshots/.gdignore


BIN
audio/midi_piano/screenshots/piano-pressed.png