Skip to content

Instantly share code, notes, and snippets.

@calebj0seph
Created March 2, 2014 12:30
Show Gist options
  • Save calebj0seph/9305873 to your computer and use it in GitHub Desktop.
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.
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