﻿﻿'''
/////////////////////////////////////////////////////////////////////////////////////////////////////////
Simple audio visualization script for Blender - version 1.0

Author: Christophe Leblanc < christopheleblanc@gmx.com >
Tested on Blender 2.79b

Original script created by sirrandalot for Blender 2.71
Video: https://www.youtube.com/watch?v=JDaSk2mX7HU
Script: https://drive.google.com/file/d/0BxG1jZ_yCoqZc251T1NpRzFwaGM/view

Feel free to modify this script to suit your needs
/////////////////////////////////////////////////////////////////////////////////////////////////////////
'''

#Configuration of the script

#Raw String - Path to the audio file (Do not forget to use raw string with the prefix 'r' or 'R' to treat backslash (\) as a literal character).
audio_file_path = R''

#String - Type of mesh used to display a bar. "PLANE" = Plane, "CUBE" = Cube.
bar_mesh_type = 'CUBE'

#Number - The number of bars to display.
number_of_bars = 64

#Number - Maximum height of a bar.
bars_max_height = 20

#None/Scale3D - The scale of the group, or "None" (default scale). (Help: *1)
bars_group_scale = None

#Boolean - Define if the visualizer is bipolar / symetric or not.
bars_bipolar = False

#Boolean - Define if the script must generate a second group of bars.
generate_clone = False

#Boolean - Define if the second group of bars must be inverted on axis X with a negative Scale X value. Used only if 'generate_clone' is True.
clone_inverted_x = False

#Boolean - Define if the second group of bars must be inverted on axis Y with a negative Scale Y value. Used only if 'generate_clone' is True.
clone_inverted_y = False

#Number - The offset between the two groups on the axis Y. Used only if 'generate_clone' is True.
clone_offset_y = 0.0

#Boolean - Define if the visualizer is circular or horizontal.
generate_circular = False

#Number/List/Tuple - Define the scaling values of the circular curve. Used only if 'generate_circular' is True. (Help: *1)
circular_curve_scale = 10

#Boolean - Set if the audio file must be added to the video sequencer
add_audio_to_video_sequencer = True

#Number - The number of the track on which the audio file is to be added in the video sequencer. Used only if 'add_audio_to_video_sequencer' is True.
video_sequencer_track = 0

#String - The name of the audio strip in the video sequencer.
video_sequencer_strip_name = 'Audio'

#None/String - Set if the script must create a new material, use an existing material, or use no material at all. (Help: *2)
material_policy = 'Create'

#String - The name of the material.
material_name = 'Material'

#String - The name of the group containing the bars.
group_name = 'Group'


#Help

# *1 Use scale configuration:
#    Number or list/tuple with only one value defines x and y. List or tuple with two or more values defines x, y and z. 
#    Examples: None  /  10  /  (4.0, 6.0)  /  [4.0, 6.0, 1.0]

# *2 Use 'material_policy':
#    None / String "None" = No material, String "Create" = Create a new material, String "Use" = Use an existing material.


'''
/////////////////////////////////////////////////////////////////////////////////////////////////////////
'''

#Script
import bpy

#Search for an existing material.
def get_existing_material(name):
    for mat in bpy.data.materials:
        if mat.name == material_name:
            return mat
    return None

#Convert a given scale config variable to a compatible list
def scale_config_to_compatible(scale):
    if isinstance(scale, list) or isinstance(scale, tuple):
        if len(scale) > 2:
            return [scale[0], scale[1], scale[2]]
        elif len(scale) == 2:
            return [scale[0], scale[1], 1.0]
        else:
            return [scale[0], scale[0], 1.0]
    elif isinstance(scale, (int, float)): #Check if the value is a number
        return [scale, scale, 1.0]
    else:
        return [1.0, 1.0, 1.0]

#Generate a group of bars
def generate_bars_group(fscene, fgroup_name, fdisplay_circular, fcircle_group, fcircle_object, fnumber_of_bars, fmaterial, inverted_x, inverted_y):
    
    global bars_bipolar, bars_group_scale, bars_max_height
    
    #Create a group for the bars
    bars_group = bpy.data.objects.new(fgroup_name, None)
    bars_group.dupli_type = 'GROUP'
    fscene.objects.link(bars_group)
    	
    #Calculate the central position of the group
    group_center_x = ((fnumber_of_bars - 1) + ((fnumber_of_bars - 1) * 0.5)) / 2
    
    #Calculate the frequency coefficient of a bar
    bar_freq_coeff = 64 / fnumber_of_bars
    
    #Calculate the Y position of the meshes
    if bars_bipolar is True:
        mesh_location_y = 0
        mesh_scale_y = bars_max_height / 2
    else:
        mesh_location_y = 1
        mesh_scale_y = bars_max_height

    #if inverted is True:
        #mesh_scale_y = 0 - mesh_scale_y
    
    #Check if the visualization must be circular and if all needed variables are setted
    if fdisplay_circular is True and fcircle_group is not None and fcircle_object is not None:
        circular_args = True
    else:
        circular_args = False

    #Set the group of the bars into the parent group if the visualizer is Circular
    if circular_args is True:
        bars_group.parent = fcircle_group
    
    #Iterate through however many bars you want
    for i in range(0, fnumber_of_bars):
        
        mesh_location_x = (i + (i * 0.5)) - group_center_x
    		
        #Add a mesh and set it's origin to one of its edges
        if bar_mesh_type is 'CUBE':
            bpy.ops.mesh.primitive_cube_add(location = (mesh_location_x, mesh_location_y, 0)) #CUBE
        else:
            bpy.ops.mesh.primitive_plane_add(location = (mesh_location_x, mesh_location_y, 0)) #PLANE
        
        active_object = bpy.context.active_object
        
        fscene.cursor_location = active_object.location
        if bars_bipolar is True:
            fscene.cursor_location.y = 0
        else:
            fscene.cursor_location.y -= 1
        bpy.ops.object.origin_set(type = 'ORIGIN_CURSOR')
        
        #Scale the plane on the x and y axis, then apply the transformation
        active_object.scale.x = 0.5
        active_object.scale.y = mesh_scale_y
        bpy.ops.object.transform_apply(location = False, rotation = False, scale = True)
        
        #Insert a scaling keyframe and lock the x and z axis
        bpy.ops.anim.keyframe_insert_menu(type = 'Scaling')
        active_object.animation_data.action.fcurves[0].lock = True
        active_object.animation_data.action.fcurves[2].lock = True
        
        #Set the window context to the graph editor
        bpy.context.area.type = 'GRAPH_EDITOR'
        
        #Expression to determine the frequency range of the bars
        l = (i * bar_freq_coeff) ** 2
        h = ((i * bar_freq_coeff) + 1) ** 2
        
    	#Print in the console
        #print(str(i) + ' ' + str(l) + ' ' + str(h)) #Debug
        
        #Bake that range of frequencies to the current plane (along the y axis)
        bpy.ops.graph.sound_bake(filepath = audio_file_path, low = (l), high = (h))
        
        #Lock the y axis
        active_object.animation_data.action.fcurves[1].lock = True
        
        #Set the material
        if fmaterial is not None:
            active_object.data.materials.append(fmaterial)
    	
    	#Set the object into the group
        active_object.parent = bars_group
        
        #Add curve constraint if the visualization is circular
        if circular_args is True:
            modifier = active_object.modifiers.new(name = "Curve", type = 'CURVE')
            modifier.object = fcircle_object
    
    bars_group.scale = scale_config_to_compatible(bars_group_scale)
    
    if inverted_x is True:
        bars_group.scale.x = 0 - bars_group.scale.x

    if inverted_y is True:
        bars_group.scale.y = 0 - bars_group.scale.y
    
    return bars_group
	
#Check if "audio_file_path" is not empty and if the file exists before running the script
if audio_file_path == "":
    audio_file_path_empty = True
else:
    audio_file_path_empty = False
    
    try:
        file = open(audio_file_path)
    except IOError:
        file_exists = False
    else:
        file_exists = True
        file.close()

if audio_file_path_empty is True:
    
    print("Audio visualization can not be created. Cause: The audio file path is empty.")
    
elif file_exists is False:
    
    print("Audio visualization can not be created. Cause: The audio file does not exists.")
    
else:
    
    print("Creation of the audio visualization is started.")
    
    #Store these variables
    scene = bpy.context.scene
    prev_area_type = bpy.context.area.type
    
    #Create a material
    if material_policy is "Create":
        material = bpy.data.materials.new(material_name)
    elif material_policy is "Use":
        material = get_existing_material(material_name)
    else:
        material = None
    
    #If the visualizer is Circular
    if generate_circular is True:
        
        #Create a parent group
        curve_group = bpy.data.objects.new(group_name, None)
        curve_group.dupli_type = 'GROUP'
        scene.objects.link(curve_group)
        
        #Create a bezier circle object
        bpy.ops.curve.primitive_bezier_circle_add(radius = 1.0, location = (0.0, 0.0, 0.0))
        curve_object = bpy.context.active_object
        curve_object.parent = curve_group

        #Define the scale of the curve
        if circular_curve_scale is not None:
            scale_values = scale_config_to_compatible(circular_curve_scale)
            curve_object.scale = (scale_values[0], scale_values[1], scale_values[2])

    else:
        curve_group = None
        curve_object = None

    #Generate the group of bars
    bars_group = generate_bars_group(scene, group_name, generate_circular, curve_group, curve_object, number_of_bars, material, False, False)
    
    if generate_clone is True:

        #Generate the second group of bars
        bars_group_2 = generate_bars_group(scene, group_name, generate_circular, curve_group, curve_object, number_of_bars, material, clone_inverted_x, clone_inverted_y)

        #Apply an offset on axis y
        bars_group.location = (0, clone_offset_y, 0)
        bars_group_2.location = (0, 0 - clone_offset_y, 0)

        bars_group = bars_group_2
    
    #Add the sound file to the video sequencer if the variable 'add_audio_to_video_sequencer' is True
    if add_audio_to_video_sequencer is True:
        
        if not scene.sequence_editor:
            scene.sequence_editor_create()
        
        scene.sequence_editor.sequences.new_sound(video_sequencer_strip_name, audio_file_path, video_sequencer_track, 1)
    
    #Unselect all selected objects
    for obj in bpy.data.objects:
        obj.select = False
    
	#Choose the parent group to select and activate
    if generate_circular is True:
        parent_group = curve_group
    else:
        parent_group = bars_group
    
    #Activate and select the group of the visualization
    bpy.context.scene.objects.active = parent_group
    parent_group.select = True
	
    #Restore the previous area
    bpy.context.area.type = prev_area_type
	
    print("Audio visualization has been correctly created.")
    print("") #Add an empty line