First commit
This commit is contained in:
319
operator_add_speakers_to_objects.py
Normal file
319
operator_add_speakers_to_objects.py
Normal file
@@ -0,0 +1,319 @@
|
||||
import bpy
|
||||
|
||||
from bpy.types import Operator
|
||||
from bpy.props import BoolProperty, StringProperty, EnumProperty, FloatProperty
|
||||
|
||||
import bpy
|
||||
from aud import Sound
|
||||
import numpy
|
||||
from numpy.linalg import norm
|
||||
from random import choice, uniform, gauss
|
||||
from math import floor
|
||||
from enum import IntFlag, Enum
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from typing import List
|
||||
|
||||
import sys
|
||||
|
||||
bl_info = {
|
||||
"name": "Add Sounds to Objects",
|
||||
"description": "Adds sounds to selected mesh objects",
|
||||
"author": "Jamie Hardt",
|
||||
"version": (0, 21),
|
||||
"blender": (2, 90, 0),
|
||||
"category": "Object",
|
||||
}
|
||||
|
||||
class SoundBank:
|
||||
def __init__(self, prefix):
|
||||
self.prefix = prefix
|
||||
self.cached_info = {}
|
||||
|
||||
def sounds(self) -> List[Sound]:
|
||||
return [sound for sound in bpy.data.sounds if sound.name.startswith(self.prefix)]
|
||||
|
||||
def random_sound(self) -> Sound:
|
||||
return choice(self.sounds())
|
||||
|
||||
def get_audiofile_info(self, sound, fps) -> (int, int):
|
||||
"""
|
||||
Returns frame of max_peak and duration in frames
|
||||
"""
|
||||
|
||||
if sound.filepath in self.cached_info.keys():
|
||||
return self.cached_info[sound.filepath]
|
||||
|
||||
if sound.filepath.startswith("//"):
|
||||
path = bpy.path.abspath(sound.filepath)
|
||||
else:
|
||||
path = sound.filepath
|
||||
|
||||
aud_sound = Sound(path)
|
||||
samples = aud_sound.data()
|
||||
sample_rate = aud_sound.specs[0]
|
||||
index = numpy.where(samples == numpy.amax(samples))[0][0]
|
||||
max_peak_frame = (index * fps / sample_rate)
|
||||
|
||||
sample_count = aud_sound.length
|
||||
frame_length = sample_count * fps / sample_rate
|
||||
|
||||
retvalue = (max_peak_frame , frame_length)
|
||||
self.cached_info[sound.filepath] = retvalue
|
||||
|
||||
return retvalue
|
||||
|
||||
class TriggerMode(str, Enum):
|
||||
START_FRAME = "START_FRAME"
|
||||
MIN_DISTANCE = "MIN_DISTANCE"
|
||||
RANDOM = "RANDOM"
|
||||
RANDOM_GAUSSIAN = "RANDOM_GAUSSIAN"
|
||||
|
||||
@dataclass
|
||||
class SpatialEnvelope:
|
||||
considered_range: float
|
||||
enters_range: int
|
||||
closest_range: int
|
||||
min_distance: float
|
||||
exits_range: int
|
||||
|
||||
def sound_camera_spatial_envelope(scene: bpy.types.Scene, speaker_obj, considered_range: float) -> SpatialEnvelope:
|
||||
min_dist = sys.float_info.max
|
||||
min_dist_frame = scene.frame_start
|
||||
enters_range_frame = None
|
||||
exits_range_frame = None
|
||||
|
||||
in_range = False
|
||||
for frame in range(scene.frame_start, scene.frame_end + 1):
|
||||
scene.frame_set(frame)
|
||||
rel = speaker_obj.matrix_world.to_translation() - scene.camera.matrix_world.to_translation()
|
||||
dist = norm(rel)
|
||||
|
||||
if dist < considered_range and not in_range:
|
||||
enters_range_frame = frame
|
||||
in_range = True
|
||||
|
||||
if dist < min_dist:
|
||||
min_dist = dist
|
||||
min_dist_frame = frame
|
||||
|
||||
if dist > considered_range and in_range:
|
||||
exits_range_frame = frame
|
||||
in_range = False
|
||||
break
|
||||
|
||||
return SpatialEnvelope(considered_range=considered_range,
|
||||
enters_range=enters_range_frame,
|
||||
exits_range=exits_range_frame,
|
||||
closest_range=min_dist_frame,
|
||||
min_distance=min_dist)
|
||||
|
||||
def closest_approach_to_camera(scene, speaker_object):
|
||||
max_dist = sys.float_info.max
|
||||
at_time = scene.frame_start
|
||||
for frame in range(scene.frame_start, scene.frame_end + 1):
|
||||
scene.frame_set(frame)
|
||||
rel = speaker_object.matrix_world.to_translation() - scene.camera.matrix_world.to_translation()
|
||||
dist = norm(rel)
|
||||
|
||||
if dist < max_dist:
|
||||
max_dist = dist
|
||||
at_time = frame
|
||||
|
||||
return (max_dist, at_time)
|
||||
|
||||
|
||||
def track_speaker_to_camera(speaker, camera):
|
||||
camera_lock = speaker.constraints.new('TRACK_TO')
|
||||
camera_lock.target = bpy.context.scene.camera
|
||||
camera_lock.use_target_z = True
|
||||
|
||||
|
||||
def random_sound_startswith(prefix):
|
||||
sounds = [sound for sound in bpy.data.sounds if sound.name.startswith(prefix)]
|
||||
return choice(sounds)
|
||||
|
||||
|
||||
def spot_audio(context, speaker, trigger_mode, sync_peak, sound_peak, sound_length, gaussian_stddev, envelope):
|
||||
|
||||
if trigger_mode == TriggerMode.START_FRAME:
|
||||
audio_scene_in = context.scene.frame_start
|
||||
|
||||
elif trigger_mode == TriggerMode.MIN_DISTANCE:
|
||||
audio_scene_in = envelope.closest_range
|
||||
|
||||
elif trigger_mode == TriggerMode.RANDOM:
|
||||
audio_scene_in = floor(uniform(context.scene.frame_start, context.scene.frame_end))
|
||||
elif trigger_mode == TriggerMode.RANDOM_GAUSSIAN:
|
||||
mean = (context.scene.frame_end - context.scene.frame_start) / 2
|
||||
audio_scene_in = floor(gauss(mean, gaussian_stddev))
|
||||
|
||||
target_strip = speaker.animation_data.nla_tracks[0].strips[0]
|
||||
|
||||
if sync_peak:
|
||||
peak = int(sound_peak)
|
||||
audio_scene_in = audio_scene_in - peak
|
||||
|
||||
audio_scene_in = max(audio_scene_in, 0)
|
||||
|
||||
# we have to do this weird dance because setting a start time
|
||||
# that happens after the end time has side effects
|
||||
target_strip.frame_end = context.scene.frame_end
|
||||
target_strip.frame_start = audio_scene_in
|
||||
target_strip.frame_end = audio_scene_in + sound_length
|
||||
|
||||
|
||||
def sync_audio(speaker, sound, context, sync_peak, trigger_mode, gaussian_stddev, sound_bank, envelope):
|
||||
print("sync_audio entered")
|
||||
fps = context.scene.render.fps
|
||||
|
||||
audiofile_info = sound_bank.get_audiofile_info(sound, fps)
|
||||
|
||||
spot_audio(context=context, speaker=speaker, trigger_mode=trigger_mode,
|
||||
gaussian_stddev=gaussian_stddev, sync_peak=sync_peak,
|
||||
sound_peak= audiofile_info[0], sound_length=audiofile_info[1],
|
||||
envelope=envelope)
|
||||
|
||||
|
||||
def constrain_speaker_to_mesh(speaker_obj, mesh):
|
||||
speaker_obj.constraints.clear()
|
||||
location_loc = speaker_obj.constraints.new(type='COPY_LOCATION')
|
||||
location_loc.target = mesh
|
||||
location_loc.target = mesh
|
||||
|
||||
|
||||
def apply_gain_envelope(speaker_obj, envelope):
|
||||
pass
|
||||
|
||||
def add_speakers_to_meshes(meshes, context, sound=None,
|
||||
sound_name_prefix = None,
|
||||
sync_peak = False,
|
||||
trigger_mode = TriggerMode.START_FRAME,
|
||||
gaussian_stddev = 1.
|
||||
):
|
||||
|
||||
context.scene.frame_set(0)
|
||||
sound_bank = SoundBank(prefix=sound_name_prefix)
|
||||
|
||||
for mesh in meshes:
|
||||
if mesh.type != 'MESH':
|
||||
print("object is not mesh")
|
||||
continue
|
||||
|
||||
envelope = sound_camera_spatial_envelope(context.scene, mesh, considered_range=5.)
|
||||
|
||||
speaker_obj = next( (spk for spk in context.scene.objects \
|
||||
if spk.type == 'SPEAKER' and spk.constraints['Copy Location'].target == mesh ), None)
|
||||
|
||||
if speaker_obj is None:
|
||||
bpy.ops.object.speaker_add()
|
||||
speaker_obj = context.selected_objects[0]
|
||||
|
||||
constrain_speaker_to_mesh(speaker_obj, mesh)
|
||||
|
||||
if sound_name_prefix is not None:
|
||||
sound = sound_bank.random_sound()
|
||||
|
||||
if sound is not None:
|
||||
speaker_obj.data.sound = sound
|
||||
|
||||
sync_audio(speaker_obj, sound, context,
|
||||
sync_peak=sync_peak,
|
||||
trigger_mode=trigger_mode,
|
||||
gaussian_stddev=gaussian_stddev,
|
||||
sound_bank=sound_bank, envelope=envelope)
|
||||
|
||||
apply_gain_envelope(speaker_obj, envelope)
|
||||
|
||||
speaker_obj.data.update_tag()
|
||||
|
||||
track_speaker_to_camera(speaker_obj, context.scene.camera)
|
||||
|
||||
|
||||
#########
|
||||
|
||||
class AddSoundToMeshOperator(Operator):
|
||||
"""Add a speaker to each selected object"""
|
||||
bl_idname = "object.add_speakers_to_obj"
|
||||
bl_label = "Add Sounds to Meshes"
|
||||
|
||||
TRIGGER_OPTIONS = (
|
||||
(TriggerMode.START_FRAME,
|
||||
"Start Frame",
|
||||
"Sound will play on the first frame of the animation"),
|
||||
(TriggerMode.MIN_DISTANCE,
|
||||
"Minimum Distance",
|
||||
"Sound will play when the object is closest to the camera"),
|
||||
(TriggerMode.RANDOM,
|
||||
"Random",
|
||||
"Sound will play exactly once, at a random time"),
|
||||
(TriggerMode.RANDOM_GAUSSIAN,
|
||||
"Random (Gaussian)",
|
||||
"Sound will play exactly once, at a guassian random time with " +
|
||||
"stdev of 1 and mean in the middle of the animation")
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
sounds_avail = bpy.data.sounds
|
||||
return len(context.selected_objects) > 0 and len(sounds_avail) > 0
|
||||
|
||||
use_sounds: StringProperty(
|
||||
name="Sound Prefix",
|
||||
description="Sounds having names starting with thie field will be assigned randomly to each speaker"
|
||||
)
|
||||
|
||||
sync_audio_peak: BoolProperty(
|
||||
name="Sync Audio Peak",
|
||||
default=True,
|
||||
description="Synchronize speaker audio to loudest peak instead of beginning of file"
|
||||
)
|
||||
|
||||
trigger_mode: EnumProperty(
|
||||
items=TRIGGER_OPTIONS,
|
||||
name="Trigger",
|
||||
description="Select when each sound will play",
|
||||
default=TriggerMode.MIN_DISTANCE,
|
||||
|
||||
)
|
||||
|
||||
gaussian_stddev: FloatProperty(
|
||||
name="Gaussian StDev",
|
||||
description="Standard Deviation of Gaussian random time",
|
||||
default=1.,
|
||||
min=0.001,
|
||||
max=6.,
|
||||
)
|
||||
|
||||
def invoke(self, context, event):
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
def execute(self, context):
|
||||
|
||||
add_speakers_to_meshes(bpy.context.selected_objects, bpy.context,
|
||||
sound=None,
|
||||
sound_name_prefix=self.use_sounds,
|
||||
trigger_mode=self.trigger_mode,
|
||||
sync_peak=self.sync_audio_peak,
|
||||
gaussian_stddev=self.gaussian_stddev)
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
def menu_func(self, context):
|
||||
self.layout.operator(AddSoundToMeshOperator.bl_idname, icon='SPEAKER')
|
||||
|
||||
|
||||
def register():
|
||||
bpy.utils.register_class(AddSoundToMeshOperator)
|
||||
bpy.types.VIEW3D_MT_object.append(menu_func)
|
||||
|
||||
|
||||
def unregister():
|
||||
bpy.utils.unregister_class(AddSoundToMeshOperator)
|
||||
bpy.types.VIEW3D_MT_object.remove(menu_func)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
register()
|
||||
|
||||
Reference in New Issue
Block a user