Skip to content

Instantly share code, notes, and snippets.

@AdmTal
Created January 21, 2024 00:53
Show Gist options
  • Save AdmTal/0dc97f4ef9b2eeaa28d26e50ffe27bb9 to your computer and use it in GitHub Desktop.
Save AdmTal/0dc97f4ef9b2eeaa28d26e50ffe27bb9 to your computer and use it in GitHub Desktop.
Animate Blender 3d piano
import bpy
import pretty_midi
from collections import defaultdict
# Parameters
BUFFER = 12
fps = 24
midi_file_path = 'wii-music.mid'
keys_collection = bpy.data.collections['Keys']
key_press_action = bpy.data.actions['Key Press']
def get_note_start_times_in_frames(midi_file_path, fps):
midi_data = pretty_midi.PrettyMIDI(midi_file_path)
if len(midi_data.instruments) > 1:
raise Exception("MIDI file contains more than one track.")
notes_frames = defaultdict(list)
for note in midi_data.instruments[0].notes:
start_frame = int(note.start * fps)
end_frame = int(note.end * fps)
notes_frames[note.pitch].append((start_frame + BUFFER, end_frame + BUFFER))
return notes_frames
def get_delta_transforms(action, frame):
delta_loc = [0.0, 0.0, 0.0]
delta_rot = [0.0, 0.0, 0.0]
for fcurve in action.fcurves:
for keyframe_point in fcurve.keyframe_points:
if keyframe_point.co.x == frame:
if fcurve.data_path == 'delta_location':
delta_loc[fcurve.array_index] = keyframe_point.co.y
elif fcurve.data_path == 'delta_rotation_euler':
delta_rot[fcurve.array_index] = keyframe_point.co.y
return delta_loc, delta_rot
def set_keyframe(obj, frame, delta_loc, delta_rot):
# Reset delta transformations before applying new ones
obj.delta_location = (0, 0, 0)
obj.delta_rotation_euler = (0, 0, 0)
# Apply new delta transformations
obj.delta_location = delta_loc
obj.delta_rotation_euler = delta_rot
# Insert keyframes for all axes
for i in range(3): # XYZ
obj.keyframe_insert(data_path="delta_location", index=i, frame=frame)
obj.keyframe_insert(data_path="delta_rotation_euler", index=i, frame=frame)
def animate_piano_keys(note_frames, keys_collection, KEY_UP_POSITION, KEY_DOWN_POSITION):
# For every Key
for key_object in keys_collection.objects:
# For the notes played on this Key
key_notes = note_frames.get(int(key_object.name), [])
# I forget what this means
prev_up_frame = 0
anim_length = 3
for down_frame, up_frame in key_notes:
# Push Key Down
# Before can push key down, it needs to be UP
PREV_UP = max(down_frame - anim_length, prev_up_frame)
set_keyframe(key_object, PREV_UP, *KEY_UP_POSITION) # UP
set_keyframe(key_object, down_frame, *KEY_DOWN_POSITION) # DOWN
set_keyframe(key_object, up_frame-anim_length, *KEY_DOWN_POSITION) # STAY DOWN
set_keyframe(key_object, up_frame, *KEY_UP_POSITION) # UP
prev_up_frame = up_frame
# Get delta transformations from the first and last keyframes of the actions
KEY_UP_POSITION = get_delta_transforms(key_press_action, key_press_action.frame_range[0])
KEY_DOWN_POSITION = get_delta_transforms(key_press_action, key_press_action.frame_range[1])
print(f'KEY_UP_POSITION -- {KEY_UP_POSITION}')
print(f'KEY_DOWN_POSITION -- {KEY_DOWN_POSITION}')
# Iterate through all objects in the scene
for obj in bpy.data.objects:
# Check if the object has animation data
if obj.animation_data:
# Clear the animation data
obj.animation_data_clear()
# Get note frames and animate keys
note_frames = get_note_start_times_in_frames(midi_file_path, fps)
animate_piano_keys(note_frames, keys_collection, KEY_UP_POSITION, KEY_DOWN_POSITION)
# Iterate through all objects in the scene
for obj in bpy.data.objects:
# Check if the object has animation data and NLA tracks
if obj.animation_data and obj.animation_data.nla_tracks:
for track in obj.animation_data.nla_tracks:
for strip in track.strips:
strip.extrapolation = 'NOTHING'
print("Extrapolation mode set to 'Nothing' for all NLA strips.")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment