Created
March 2, 2014 12:30
-
-
Save calebj0seph/9305873 to your computer and use it in GitHub Desktop.
An audio visualisation generator. To use, open Blender and go to the Text Editor. Copy/paste the code and call bake_visualisation_circle() with the desired parameters.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import bpy | |
from math import e, pi, cos, sin | |
def frequency_distribution(min_val, max_val, step, max_step): | |
sharpness = 6 | |
return min_val + ((e**((sharpness*step)/max_step) - 1)/(e**sharpness - 1)) * (max_val - min_val) | |
def bake_visualisation_circle(bars: int=50, repetitions: int=3, | |
radius: float=2.0, bar_depth: float=1.0, bar_height: float=2.0, | |
bar_offset: float=0.0, bar_separation: float=0.0, | |
min_frequency: float=48.0, max_frequency: float=17000.0, file: str="", | |
context=bpy.context): | |
""" | |
Generates an audio visualisation in the form of a circle, with bars | |
extruding from the centre of the circle. | |
Keyword arguments: | |
bars -- the number of bars to visualise, higher values provide more detail | |
but longer baking times (default 50) | |
repetitions -- the number of times to repeat the set of bars around the | |
circle (default 3) | |
radius -- the distance from the midpoint of the circle at which bars are | |
placed (default 2.0) | |
bar_depth -- a scaling factor of the depth of the bars perpendicular to the | |
plane of the circle (default 1.0) | |
bar_height -- the maximum length the bars are able to reach in the | |
visualisation (default 2.0) | |
bar_offset -- the midpoint that each bar will extrude around in the range | |
of [-0.5, 0.5], with lower values causing bars to extrude | |
outwards more, and higher values causing the bars to extrude | |
inwards more (default 0.0) | |
bar_separation -- the distance between each bar on the circle in the range | |
of [0.0, 1.0), with a value of 0.0 causing no separation, | |
and a value of 0.5 causing separation equal to the width | |
of each bar (default 0.0) | |
min_frequency -- the lowest frequency to be visualised in Hz (default 48.0) | |
max_frequency -- the highest frequency to be visualised in Hz (default | |
17000.0) | |
file -- the path to a FFmpeg compatible audio file to be visualised | |
""" | |
# Set visualisation starting frame to 0 | |
context.scene.frame_current = 0 | |
# Switch to 3D Viewport context | |
original_area_type = context.area.type | |
context.area.type = 'VIEW_3D' | |
# Add an empty to use as the origin point for the mirror modifiers | |
bpy.ops.object.empty_add( | |
location=(0.0, 0.0, 0.0) | |
) | |
empty_origin = context.active_object | |
empty_origin.name = "Origin" | |
side_angle = (2*pi / repetitions) | |
# Add initial cube mesh | |
bpy.ops.mesh.primitive_cube_add( | |
radius=0.5, | |
location=(0.0, 0.0, 0.0) | |
) | |
old_cursor = context.scene.cursor_location | |
context.scene.cursor_location = (0.0, 0.0, bar_offset) | |
bpy.ops.object.origin_set(type="ORIGIN_CURSOR") | |
context.scene.cursor_location = old_cursor | |
cube_mesh = context.active_object | |
cube_mesh.scale.x = side_angle / 2 * radius / bars * (1 - bar_separation) | |
cube_mesh.scale.z = bar_height | |
cube_mesh.scale.y = cube_mesh.scale.x * bar_depth | |
bpy.ops.object.transform_apply(scale=True) | |
cube_mesh.data.name = "BarMesh" | |
# Add mirror modifier | |
bpy.ops.object.modifier_add(type="MIRROR") | |
cube_mesh.modifiers["Mirror"].mirror_object = empty_origin | |
cube_mesh.modifiers["Mirror"].use_mirror_merge = False | |
cube_mesh.modifiers["Mirror"].use_mirror_vertex_groups = False | |
cube_mesh.modifiers["Mirror"].use_x = False | |
cube_mesh.modifiers["Mirror"].use_y = False | |
cube_mesh.modifiers["Mirror"].use_z = True | |
# Add array modifier to cube for repetition around the circle | |
bpy.ops.object.modifier_add(type="ARRAY") | |
cube_mesh.modifiers["Array"].fit_type = "FIXED_COUNT" | |
cube_mesh.modifiers["Array"].count = repetitions | |
cube_mesh.modifiers["Array"].use_constant_offset = False | |
cube_mesh.modifiers["Array"].use_relative_offset = False | |
cube_mesh.modifiers["Array"].use_merge_vertices = False | |
cube_mesh.modifiers["Array"].use_object_offset = True | |
# Add material slot to cube | |
bpy.ops.object.material_slot_add() | |
cube_mesh.material_slots[0].link = "OBJECT" | |
# Add a constraint to the cube to copy the scale from the controllers | |
bpy.ops.object.constraint_add(type="COPY_SCALE") | |
cube_mesh.constraints[0].use_x = False | |
cube_mesh.constraints[0].use_y = False | |
cube_mesh.constraints[0].use_z = True | |
# Create shared node group material | |
node_group = bpy.data.node_groups.new("Visualisation", "ShaderNodeTree") | |
group_inputs = node_group.nodes.new("NodeGroupInput") | |
group_inputs.location = (-200.0, 0.0) | |
node_group.inputs.new("NodeSocketFloat", "Intensity") | |
emission = node_group.nodes.new("ShaderNodeEmission") | |
emission.inputs["Color"].default_value = (1.0, 1.0, 1.0, 1.0) | |
group_outputs = node_group.nodes.new("NodeGroupOutput") | |
group_outputs.location = (200.0, 0.0) | |
node_group.outputs.new("NodeSocketShader", "Shader") | |
node_group.links.new( | |
emission.inputs["Strength"], | |
group_inputs.outputs["Intensity"] | |
) | |
node_group.links.new( | |
group_outputs.inputs["Shader"], | |
emission.outputs["Emission"] | |
) | |
for i in range(bars + 1): | |
theta = (side_angle / 2)/bars * i | |
# Add controller as an empty | |
bpy.ops.object.empty_add( | |
location=(cos(theta + side_angle) * radius, 0.0, sin(theta + side_angle) * radius), | |
rotation=(0.0, -(2*pi / repetitions - pi/2 + theta), 0.0) | |
) | |
empty = context.active_object | |
empty.name = ("Controller.%%0%id" % len(str(bars + 1))) % (i,) | |
# Duplicate cube_mesh as cube | |
empty.select = False | |
cube_mesh.select = True | |
context.scene.objects.active = cube_mesh | |
bpy.ops.object.duplicate(linked=True) | |
cube = context.active_object | |
cube.name = ("Bar.%%0%id" % len(str(bars + 1))) % (i,) | |
# Update object-specific properties of cube | |
cube.modifiers["Array"].offset_object = empty | |
cube.constraints["Copy Scale"].target = empty | |
cube.location = (cos(theta) * (radius + 0.0), 0.0, sin(theta) * (radius + 0.0)) | |
cube.rotation_euler = (0.0, pi/2 - theta, 0.0) | |
# Remove mirror modifier for first and last bars | |
if i == 0 or i == bars: | |
cube.modifiers.remove(cube.modifiers["Mirror"]) | |
# Add new material to cube and clear existing nodes | |
bpy.ops.material.new() | |
cube.material_slots[0].material = bpy.data.materials[-1] | |
cube.material_slots[0].material.use_nodes = True | |
cube.material_slots[0].material.node_tree.nodes.clear() | |
# Add a Value node to the material and assign it a driver controlled by the scale of empty | |
value_node = cube.material_slots[0].material.node_tree.nodes.new("ShaderNodeValue") | |
value_node.location = (-200.0, 0.0) | |
driver = value_node.outputs["Value"].driver_add("default_value").driver | |
driver.type = "SUM" | |
for variable in driver.variables: | |
driver.variables.remove(variable) | |
driver_var = driver.variables.new() | |
driver_var.name = "Scale" | |
driver_var.type = "SINGLE_PROP" | |
driver_var.targets[0].id_type = "OBJECT" | |
driver_var.targets[0].id = empty | |
driver_var.targets[0].data_path = "scale.z" | |
# Link Value node to node_group | |
vis_node = cube.material_slots[0].material.node_tree.nodes.new("ShaderNodeGroup") | |
vis_node.node_tree = node_group | |
out_node = cube.material_slots[0].material.node_tree.nodes.new("ShaderNodeOutputMaterial") | |
out_node.location = (200.0, 0.0) | |
cube.material_slots[0].material.node_tree.links.new( | |
vis_node.inputs["Intensity"], | |
value_node.outputs["Value"] | |
) | |
cube.material_slots[0].material.node_tree.links.new( | |
out_node.inputs["Surface"], | |
vis_node.outputs["Shader"] | |
) | |
# Bake the current frequency step to the empty controller | |
cube.select = False | |
empty.select = True | |
context.scene.objects.active = empty | |
bpy.ops.anim.keyframe_insert(type='Scaling', confirm_success=False) | |
context.area.type = 'GRAPH_EDITOR' | |
empty.animation_data.action.fcurves[0].select = False | |
empty.animation_data.action.fcurves[1].select = False | |
range_low = frequency_distribution(min_frequency, max_frequency, i, bars + 1) | |
range_high = frequency_distribution(min_frequency, max_frequency, i + 1, bars + 1) | |
bpy.ops.graph.sound_bake(filepath=file, low=range_low, high=range_high) | |
context.area.type = 'VIEW_3D' | |
empty.select = False | |
cube_mesh.select = True | |
context.scene.objects.active = cube_mesh | |
print("%.2f%% complete" % ((i+1)/(bars+1)*100)) | |
bpy.ops.object.delete() | |
context.area.type = original_area_type |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment