Showing posts with label software. Show all posts
Showing posts with label software. Show all posts

Thursday, August 29, 2019

Using MIDI and Max to Generate Keyframes for Apple Motion

There may not be anyone else on the planet (current population 7.7 billion) who wants to do this, but in case you are out there, here is a kludge of a solution...

For my Estuaries series of audiovisual compositions, I in some cases used data from my Nodewebba software and Max not only to control music but also to control the visuals, which I was authoring in Apple Motion (v. 5.3).

I did this by capturing the needed data from Nodwebba as 14-bit pitch-bend data into the sequencer where I was authoring/editing the music. The picture below shows Max code taking the six floating-point outputs of the Nodewebba nodes, scaling it to the range of 14-bit pitch bend, and sending it out via the "from Max 2" port, with a different MIDI channel for each stream of data. My MIDI sequencer, with transport locked to control Nodewebba, received the data and recorded it on six separate tracks.



I then wrote Max code that would receive this MIDI data, scale the 16-bit pitch bend values to desired values for Motion keyframes, and write a text file containing Motion XML keyframe specifications (as described in the Motion XML File Format document available from the Apple Developer site). Since the keyframes must be given specific time values, the Max code must lock to the sequencer transport and calculate absolute time. This was achieved using a hostsync~ to receive the transport activation. For reasons I don't recall, I ended up using clocker to generate elapsed time rather than trying to calculate elapsed time from hostsync~. This remained sufficiently accurate over short periods of time.

I have posted the Max code for converting a timestamp and value to Motion XML (bat.keypointGen) on my website under Software: Max/MSP/Jitter tools. This code gathers the keyframe XML entries into a text object, and the contents can then be written to a text file.

There is an important trick in this code: Motion uses rational time values to give very precise time indications that are independent of FPS (I believe). For example, here is a time from a keyframe in Motion XML:
value: <time>3937280 153600 1 0</time>
To explain this, it is probably best for me to just quote Apple's Darrin Cardini answering my query to the ProAppsDev support list, where I was trying to understand the 2831360 153600 1 0 given for a keyframe at frame 554 in a 30 fps project:
The times you’re seeing are rational times. The first 2 numbers are a numerator and denominator. The OS has a rational time type named CMTime, defined in the CoreMedia.framework headers (see CMTime.h for details). So the value you list below is 2831360 / 153600 = 18.433.. seconds. 554 frames at 30fps = 18.46666666666667. The difference between those is 0.03… or 1/30th of a second, or 1 frame. That’s because frame numbering starts at 1, but time starts at 0, I think, so they’ll always differ by 1 frame time.
The extra 1 and 0 you see after the numbers in the time are also described in the CMTime structure. The 1 is a flag that says the structure is valid, and the 0 is the epoch (or more mundanely, the loop number). For most purposes you will want to keep these values at 1 and 0, respectively. 
To generate one of these times yourself, you’ll want to pick a timebase (denominator) that makes your life easier. Let’s say you have a project that is 1080p30. You could just use 30 for the denominator, and each frame would be addressed as 0 / 30, 1 / 30, 2 / 30, etc. However, as soon as you need to deal with any footage or projects at a different frame rate than 30, you’ll run into problems. (What happens when someone drops a 30fps piece of footage into a 24fps project? Or vice-versa?) So finding a common denominator among the time bases is useful. For 24, 25, 30, 50, and 60, the timebase of 600 works quite nicely, as you can address individual frames in all of those frame rates as integer fractions with a denominator of 600. 1 frame at 24 fps = 25/600. 1 frame at 25fps = 24/600. 1 frame at 30 fps = 20/600th, etc.
I followed the example of what my Motion project was generating, and used 153600 as the denominator.  

The problem is, since Max cannot be coerced to generate " marks correctly in the text file, I had to open the Max-generated text file and search-and-replace all &-marks with "-marks. Yes, not very elegant, but it does the job.

I then opened the Motion project file as text, found the data for the particular project element I wanted to provide keyframes for, and then copied/pasted the contents of that Max-generated text file into the Motion XML. I then saved the edited project file and opened it in Motion.

Why not generate the keyframes directly from the original Nodewebba data rather than going through the intermediate sequencer capture? In the sequencer I engaged in editing, including tempo manipulations, that altered the timing of the events. Reading the node data from the sequencer, then, gave me the correct timings that the visuals needed.


Tuesday, February 16, 2016

"Replicator Grid" Blender script

This Blender script creates a 2D grid of replicated objects with a descending line of parenting control, running left to right, then down to the next row. The following video demonstrates:

 
 
# ---------
# Tested in Blender 2.75a and 2.76
# Makes a 'replicator grid' -- multiple copies of a given object, parented in a tranformation-control chain.
# Provides a single empty that controls the relative size, displacement and rotation of each successive object in relation to
# its parent.
#
# Usage: 
# Change the number of rows and columns below.
# Select target object to be replicated.
# Run the script.
# If you did this with an object named 'cube', for example,
# You will end up with a 'cube_replicator' empty, below which all of the duplicated objects
# have been parented.
# And you will have a new cube_replicator_control empty, which can be moved, scaled and rotated
# to control the overall replicator grid.

# Bret Battey / BatHatMedia.com September 2015
# ---------
# 

import bpy
from bpy.props import *

# specify number of rows and columns here!
rows = 5
columns = 5

# The following function is adapted from Nick Keeline "Cloud Generator" 
# addNewObject in object_cloud_gen.py

def duplicateObject(scene, name, copyobj):

    # Create new mesh
    mesh = bpy.data.meshes.new(name)

    # Create a new object.
    ob_new = bpy.data.objects.new(name, mesh)
    tempme = copyobj.data
    ob_new.data = tempme.copy()
    ob_new.scale = copyobj.scale
    ob_new.location = copyobj.location

    # Link new object to the given scene and select it.
    scene.objects.link(ob_new)
    ob_new.select = True

    return ob_new            


# Build the parenting chain of replications tied to empties and control 
# constraints. If we built a parented chain of objects, we wouldn't be able to 
# transform each object independently. So instead we build a parented chain of
# empties, then parent each duplicated object to one of the empties.

def gridOfReps(rows, cols, source_obj, ctrl, scene):
    for row in range(rows):
        for col in range(cols):
            #Create a locator empty
            bpy.ops.object.add(type="EMPTY", location=(0,0,0))
            rep_loc = bpy.context.object
            # The first empty becomes the root of the whole beast, and the 
            # source object gets parented to it
            if((row == 0) and (col == 0)):
                rep_loc.name = source_obj.name+"_replicator"
                parent_obj = rep_loc
                prev_row_locator = rep_loc
                source_parent = source_obj.parent
                # If the source object has a parent, locate and parent 
                # the new replicator base empty to that.
                if((source_parent) and (source_parent.type == 'EMPTY')):
                    rep_loc.location = source_parent.location
                    rep_loc.parent = source_parent
                # Otherwise just position the base empty to the source obj,
                # zero the source obj location, and parent to the base
                else:  
                    rep_loc.location = source_obj.location
                    source_obj.location = (0,0,0)
                    source_obj.parent = rep_loc   
            else: 
                rep_loc.name = "rep_loc_r"+str(row)+"c_"+str(col)  # name can't 
                    #be specified via the .add method
                rep_loc.parent = parent_obj # parent to the previous locator
                # First locator in each row other than 0 gets special treatment
                if((row > 0) and (col == 0)):
                    # First location constraint locks to the prev. row locator
                    constraint = rep_loc.constraints.new("COPY_LOCATION")  
                    constraint.target = prev_row_locator
                    constraint.use_x = True
                    constraint.use_y = True
                    constraint.use_z = True       
                    constraint.owner_space = "WORLD"
                    constraint.target_space = "WORLD"
                    constraint.influence = 1.0
                    # Second location constraint implements the z offset 
                    # from the controller      
                    constraint = rep_loc.constraints.new("COPY_LOCATION")  
                    constraint.target = ctrl
                    constraint.use_x = False 
                    constraint.use_y = False
                    constraint.use_z = True       
                    constraint.owner_space = "LOCAL"
                    constraint.target_space = "WORLD"
                    constraint.influence = 1.0
                    constraint.use_offset = True   # use the controller's 
                    # location as an offset rather than an absolute location
                    # And ready this locator to be basis of next row position
                    prev_row_locator = rep_loc
                else:
                    # For other items in a row, offset location x on basis of 
                    # controller
                    constraint = rep_loc.constraints.new("COPY_LOCATION")  
                    constraint.target = ctrl
                    constraint.use_x = True  # for offsetting a row, only x is 
                        # drawn from the controller 
                    constraint.use_y = False
                    constraint.use_z = False       
                    constraint.owner_space = "LOCAL"
                    constraint.target_space = "WORLD"
                    constraint.influence = 1.0
                    constraint.use_offset = True   # use the controller's 
                        #location as an offset rather than an absolute location
                # For all locators except the root, apply these influences from 
                    #the controller:
                # Copy_Rotation Constraint 
                constraint = rep_loc.constraints.new("COPY_ROTATION")  
                constraint.target = ctrl
                constraint.owner_space = "LOCAL"
                constraint.target_space = "WORLD"
                constraint.influence = 1.0
                # Copy_Scale Constraint. This will also cascade down the chain 
                    # due to the combined effect with the parenting 
                constraint = rep_loc.constraints.new("COPY_SCALE")  
                constraint.target = ctrl
                constraint.owner_space = "LOCAL"
                constraint.target_space = "WORLD"
                constraint.influence = 1.0
                # In all cases (except root), duplicate source object and parent
                    # to the replicator empty.
                # This strategy allows later transformations of the visible 
                    #object without influencing the children
                new_obj = duplicateObject(scene, "rep", source_obj)
                new_obj.parent = rep_loc    
                # Advance parent assignment
                parent_obj = rep_loc    
    return parent_obj

# get the source object

source_obj = bpy.context.object
source_loc = source_obj.location
source_scene = bpy.context.scene

# create and name the controller empty

bpy.ops.object.add(type="EMPTY", location=(0,0,0))
ctrl = bpy.context.object  
ctrl.name = source_obj.name+"_replicator_control"  # name can't be specified 
        #via the .add method

#replicate!
last_locator = gridOfReps(rows, columns, source_obj, ctrl, source_scene)
 

Blender Script for creating animated motion-trail ribbon

Given an object for which a motion-path has been generated, will create a Beziér-spline ribbon that follows the motion of the object for a given tail length (designated in frames).




# ---------
# Tested in Blender 2.75a and 2.76
# Makes a Bezier-spline motion trail for an object.
# Set the length of the tail in frames via the tail_length variable below.
# To run, select object, generate a motion path for the object (Editor>Motion Paths) across the total desired frame range, and Run Script.
# Warning: this can take a long time to run with long motion paths and long tail sizes
# Once the spline has been generated, extrude and/or add bevel and taper objects to the Geometry settings of the spline.
# Bret Battey / BatHatMedia.com September 2015
# ---------

import bpy
from bpy.props import *
from mathutils import *
from math import *
import time

# ----- FUNCTION DEFINITIONS -----

# Time utils thanks to http://blenderscripting.blogspot.co.uk/search/label/time

def get_last_time():  
    if len(time_list) < 2:  
        return "ERROR: must have two time entries to calculate the difference"  
    return time_list[-1] - time_list[-2]      
  
def mark_time():  
    time_list.append(time.time())      
  
time_list = [] 


##------------------------------------------------------------
#### Curve creation functions
# sets bezierhandles to auto
def setBezierHandles(obj, mode = 'AUTOMATIC'):
    scene = bpy.context.scene
    if obj.type != 'CURVE':
        return
    scene.objects.active = obj
    bpy.ops.object.mode_set(mode='EDIT', toggle=True)
    bpy.ops.curve.select_all(action='SELECT')
    bpy.ops.curve.handle_type_set(type=mode)
    bpy.ops.object.mode_set(mode='OBJECT', toggle=True)


# create new CurveObject 
def createCurve(verts, name):
      
    # create curve
    scene = bpy.context.scene  
    newCurve = bpy.data.curves.new(name, type = 'CURVE') # curvedatablock
    newSpline = newCurve.splines.new(type = 'BEZIER') # spline

    # The new spline already has one point. Add the remaining needed.
    newSpline.bezier_points.add(verts-1)

    # set curveOptions
    newCurve.dimensions = '3D'

    # create object with newCurve
    new_obj = bpy.data.objects.new(name, newCurve) # object
    scene.objects.link(new_obj) # place in active scene
    new_obj.select = True # set as selected
    scene.objects.active = new_obj  # set as active

    # set bezierhandles
    setBezierHandles(new_obj)

    return new_obj

#based on animation_rotobezier.py
def keyframeBezier(Obj, frame):

    Data = Obj.data
    
    for Spline in Data.splines:
        for CV in Spline.bezier_points:
            CV.keyframe_insert(data_path='co', frame = frame)
            CV.keyframe_insert(data_path='handle_left', frame = frame)
            CV.keyframe_insert(data_path='handle_right', frame = frame)
    
# -------------  SCRIPT START

tail_length = 60   # length of the spline in frames
object = bpy.context.object
path = object.motion_path
frame_start = path.frame_start
frame_end = path.frame_end
path_length = path.length

# --- create the Bezier curve
trail = createCurve(tail_length,object.name+" motion_trail")

mark_time() # start measuring elapsed time

# --- key frame the curve at every frame in the motion path

for i in range(tail_length-1,path_length): # motion path index
    print("Point "+str(i))
    for j in range(tail_length): # curve point index 
        trail.data.splines[0].bezier_points[j].co = path.points[(i-tail_length)+j].co # set the coordinates of the motion tail to the corresponding point on the motion path
    # keyframe it
    keyframeBezier(trail, frame_start+i)
    
mark_time()

print('The motion trail keyframing took {: 5g} seconds'.format(get_last_time()))




Blender Script for object-motion measurements

In Blender, given an object that has a motion path, will provide animated empties which indicate the object's velocity, acceleration, magnitude, and accumulated distance moved.


# ---------
# Tested on Blender 2.75a and 2.76
# For a given object, makes an empty, which moves to indicate the velocity (displacement between frames) for each axis of the original object
# and another empty providing magnitude of the movement
# and another empty providing accumulative displacement for each axis, 
# and yet another for acceleration. 
# To run, select object, generate a motion path for the object (Editor>Motion Paths) across the total desired frame range, and Run Script.
# Bret Battey / BatHatMedia.com Dec 2015
# ---------

import bpy
from bpy.props import *
from mathutils import *
from math import *
import time

# ----- FUNCTION DEFINITIONS -----

# Time utils thanks to http://blenderscripting.blogspot.co.uk/search/label/time

def get_last_time():  
    if len(time_list) < 2:  
        return "ERROR: must have two time entries to calculate the difference"  
    return time_list[-1] - time_list[-2]      
  
def mark_time():  
    time_list.append(time.time())      
  
time_list = [] 


# -------------  SCRIPT START

object = bpy.context.object
path = object.motion_path
frame_start = path.frame_start
frame_end = path.frame_end
path_length = path.length
# velocity empty
bpy.ops.object.empty_add(type='PLAIN_AXES', radius=1, view_align=False, location=(0,0,0), layers=object.layers)
velempty = bpy.context.object
velempty.name = object.name + '_velocity'
# vector magnitude 
bpy.ops.object.empty_add(type='PLAIN_AXES', radius=1, view_align=False, location=(0,0,0), layers=object.layers)
magempty = bpy.context.object
magempty.name = object.name + '_magnitude'
# accumulative distance traveled 
bpy.ops.object.empty_add(type='PLAIN_AXES', radius=1, view_align=False, location=(0,0,0), layers=object.layers)
accumempty = bpy.context.object
accumempty.name = object.name + '_accum_distance'
# acceleration 
bpy.ops.object.empty_add(type='PLAIN_AXES', radius=1, view_align=False, location=(0,0,0), layers=object.layers)
accelempty = bpy.context.object
accelempty.name = object.name + '_acceleration'

mark_time() # start measuring elapsed time

for i in range(0,path_length-2): # motion path index
    nextPointCo = path.points[i+1].co # next time point
    thisPointCo = path.points[i].co # this time point
    dif = nextPointCo-thisPointCo # x,y,z distances
    mag = sqrt(dif[0]*dif[0]+dif[1]*dif[1]+dif[2]*dif[2]) # vector magnitude via pythagorean theorem
    velempty.location = dif # set vel empty's location to the velocity vector
    magempty.location[2] = mag # set z position of mag empty's location to the magnitude
    frame = frame_start+i  # get absolute frame number
    velempty.keyframe_insert(data_path='location',frame=frame,index=-1) # set keyframe for velocity. index -1 = set all three axes
    magempty.keyframe_insert(data_path='location',frame=frame,index=2) # set keyframe for velocity. index -1 = set all three axes
    if i == 0:
        accumempty.keyframe_insert(data_path='location',frame=frame,index=-1) # set keyframe. index -1 = set all three axes
        accumdif = accumempty.location # start accumulated distance with a convenient 0,0,0 vector
    else:
        accumdif[0] += abs(dif[0]) # add absolute value of velocity to the accumulative distance
        accumdif[1] += abs(dif[1])
        accumdif[2] += abs(dif[2])
        accumempty.location = accumdif # move empty to this point
        accumempty.keyframe_insert(data_path='location',frame=frame,index=-1) # set keyframe. index -1 = set all three axes
        accelempty.location = dif-lastdif # acceleration = dif between this frame's velocity and the previous frame's
        accelempty.keyframe_insert(data_path='location',frame=frame-1,index=-1) # set keyframe ONE FRAME BACK. index -1 = set all three axes
    lastdif = dif

   
mark_time()

print('The keyframing took {: 5g} seconds'.format(get_last_time()))



Tuesday, December 22, 2015

Video Preview of My New OptiNelder Plugin

Video preview of my new OptiNelder plugin for Apple Motion, using Nelder-Mead search agents to crawl on an input image.



https://vimeo.com/149694242


Multisample Antialiasing for FxPlugs

I finally implemented OpenGL Multisample Antialiasing in the context of an Apple FxPlug project. Took me a lot of time to figure this one out (why is it so hard to find clear implementation details on MSAA?). I hope I can save someone else the pain:

https://github.com/bbattey/MSAA-for-FxPlugs




Sunday, September 29, 2013

Nodewebba Generative Music Software Now Available!

The first release of Nodewebba software is now available from BatHatMedia.com, free and open source for Macintosh and Windows.

Nodewebba brings some distinctly wild and crazy music algorithms out of the research lab and into the home and professional studio. The musician can easily create a "web" of feedback-based pattern generators, producing surprisingly fresh and dynamic interlocking melodic and rhythmic patterns for use realtime performance or for studio-based productions.

See the Welcome to Nodewebba video for a taste of what it can do: