Skip to content

Instantly share code, notes, and snippets.

@vanjac
Created January 21, 2020 11:23
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save vanjac/1bcde7d86d363114bcc336632e9b9d9e to your computer and use it in GitHub Desktop.
Save vanjac/1bcde7d86d363114bcc336632e9b9d9e to your computer and use it in GitHub Desktop.
Blender MIDI control add-on
bl_info = {
"name": "MIDI Control",
"author": "Jacob van't Hoog",
"version": (1, 0),
"blender": (2, 80, 0),
"category": "User Interface",
}
import pygame.midi
import bpy
midi_in = None
# dictionary of midi controls to values
midi_positions = {}
# dictionary of midi controls to property paths.
midi_properties = {}
queued_property = None
def get_area_context(name):
for area in bpy.context.screen.areas:
if area.type == name:
ctx = bpy.context.copy()
ctx['area'] = area
ctx['region'] = area.regions[-1]
return ctx
def change_property(full_path, amount):
# set attribute if type is integer
current_value = eval(full_path)
prop_type = type(current_value)
if prop_type is int:
exec(full_path + '=' + str(current_value + amount))
elif prop_type is float:
exec(full_path + '=' + str(current_value + float(amount) / 10.0))
return {'FINISHED'}
def midi_update():
global midi_in
if not pygame.midi.get_init():
return
try:
midi_events = midi_in.read(1023)
for midi_event in midi_events:
midi_event_data = midi_event[0]
handle_midi_event(midi_event_data)
except Exception as e:
print(e)
finally:
return 1.0/15.0 # 15 times per second
def handle_midi_event(midi_event_data):
global midi_positions, midi_properties, queued_property
command = midi_event_data[0]
data1 = midi_event_data[1]
data2 = midi_event_data[2]
if command == 176: # control change
if data1 in midi_properties and data1 in midi_positions:
change_property(midi_properties[data1], data2 - midi_positions[data1])
if queued_property:
midi_properties[data1] = queued_property
queued_property = None
midi_positions[data1] = data2
def midi_stop():
global midi_in, midi_positions, midi_properties, queued_property
print("Stop MIDI Control")
midi_positions = {}
midi_properties = {}
queued_property = {}
if midi_in is not None:
midi_in.close()
midi_in = None
if bpy.app.timers.is_registered(midi_update):
bpy.app.timers.unregister(midi_update)
if pygame.midi.get_init():
pygame.midi.quit()
class MIDIStartOperator(bpy.types.Operator):
bl_idname = "object.midi_start"
bl_label = "Start MIDI Control"
bl_options = {'REGISTER'}
def selected_device_callback(self, context):
return MIDIStartOperator._list_midi_devices(self, context)
selected_device: bpy.props.EnumProperty(items=selected_device_callback, name="Device")
def invoke(self, context, event):
midi_stop()
pygame.midi.init()
wm = context.window_manager
return wm.invoke_props_dialog(self)
def execute(self, context):
global midi_in
midi_stop()
pygame.midi.init()
input_device = int(self.selected_device)
midi_in = pygame.midi.Input(input_device)
bpy.app.timers.register(midi_update)
self.report({'INFO'}, "MIDI control started!")
return {'FINISHED'}
def _list_midi_devices(self, context):
enum_items = []
num_devices = pygame.midi.get_count()
for device_i in range(0, num_devices):
# (interface name, device name, is input?, is output?, in use?)
info = pygame.midi.get_device_info(device_i)
if info[2]: # device supports input
id = str(device_i)
name = info[1].decode("utf-8") # bytes to string
if info[4]:
name += " (in use!)"
description = info[0].decode("utf-8")
# (identifier, name, description number)
enum_items.append((id, name, description))
return enum_items
class MIDIStopOperator(bpy.types.Operator):
bl_idname = "object.midi_stop"
bl_label = "Stop MIDI Control"
bl_options = {'REGISTER'}
def execute(self, context):
midi_stop()
self.report({'INFO'}, "MIDI control stopped!")
return {'FINISHED'}
class MIDIAdjustPropertyOperator(bpy.types.Operator):
bl_idname = "screen.active_property_midi"
bl_label = "Attach MIDI control to property"
bl_options = {'REGISTER'}
@classmethod
def poll(cls, context):
return bpy.ops.ui.copy_data_path_button.poll()
def execute(self, context):
global queued_property, midi_in
if not midi_in:
self.report({'ERROR'}, "Please start MIDI control first!")
else:
# get full data path
bpy.ops.ui.copy_data_path_button(full_path=True)
queued_property = context.window_manager.clipboard
return {'FINISHED'}
class MIDIClearPropertyOperator(bpy.types.Operator):
bl_idname = "screen.active_property_midi_clear"
bl_label = "Unattach MIDI control from property"
bl_options = {'REGISTER'}
@classmethod
def poll(cls, context):
return bpy.ops.ui.copy_data_path_button.poll()
def execute(self, context):
global midi_properties, midi_in
if not midi_in:
self.report({'ERROR'}, "Please start MIDI control first!")
else:
# get full data path
bpy.ops.ui.copy_data_path_button(full_path=True)
path = context.window_manager.clipboard
if path in midi_properties.values():
index = list(midi_properties.values()).index(path)
key = list(midi_properties.keys())[index]
del midi_properties[key]
return {'FINISHED'}
def register():
midi_stop()
bpy.utils.register_class(MIDIStartOperator)
bpy.utils.register_class(MIDIStopOperator)
bpy.utils.register_class(MIDIAdjustPropertyOperator)
bpy.utils.register_class(MIDIClearPropertyOperator)
def unregister():
bpy.utils.unregister_class(MIDIStartOperator)
bpy.utils.unregister_class(MIDIStopOperator)
bpy.utils.unregister_class(MIDIAdjustPropertyOperator)
bpy.utils.unregister_class(MIDIClearPropertyOperator)
midi_stop()
if __name__ == "__main__":
register()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment