Isolating project from my addons folder

This commit is contained in:
Jamie Hardt
2020-09-29 16:35:56 -07:00
parent d3c52b56f2
commit c75de1d97d
11 changed files with 0 additions and 0 deletions

0
intern/__init__.py Normal file
View File

View File

@@ -0,0 +1,175 @@
import bpy
from numpy.linalg import norm
from random import uniform, gauss
from math import floor
from enum import Enum
from dataclasses import dataclass
import sys
from .soundbank import SoundBank
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 spot_audio(context, speaker, trigger_mode, sync_peak, sound_peak, sound_length, gaussian_stddev, envelope):
audio_scene_in = context.scene.frame_start
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)
track_speaker_to_camera(speaker_obj, context.scene.camera)
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()

218
intern/generate_adm.py Normal file
View File

@@ -0,0 +1,218 @@
import bpy
from contextlib import contextmanager
import lxml
import uuid
from fractions import Fraction
import struct
from os.path import dirname
import numpy
from time import strftime
from typing import List
from ear.fileio.utils import openBw64
from ear.fileio.bw64.chunks import (FormatInfoChunk, ChnaChunk)
from ear.fileio.adm import chna as adm_chna
from ear.fileio.adm.xml import adm_to_xml
from ear.fileio.adm.builder import (ADMBuilder)
from ear.fileio.adm.generate_ids import generate_ids
from .geom_utils import (speaker_active_time_range,
speakers_by_min_distance,
speakers_by_start_time)
from .object_mix import (ObjectMix, ObjectMixPool, object_mixes_from_source_groups)
from .speaker_utils import (all_speakers)
def group_speakers(speakers, scene) -> List[List[bpy.types.Object]]:
def list_can_accept_speaker(speaker_list, speaker_to_test):
test_range = speaker_active_time_range(speaker_to_test)
def filter_f(spk):
spk_range = speaker_active_time_range(spk)
return test_range.overlaps(spk_range)
result = next((x for x in speaker_list if filter_f(x) is True), None)
return result is None
by_priority = speakers_by_min_distance(scene, speakers)
ret_val = [[]]
for spk in by_priority:
success = False # flaggy-flag because I can't do a break->continue from the inner
for elem in ret_val:
if list_can_accept_speaker(elem, spk):
elem.append(spk)
success = True
break
if not success:
ret_val.append([spk])
for i in range(len(ret_val)):
ret_val[i] = speakers_by_start_time(ret_val[i])
return ret_val
def adm_for_object(scene, sound_object: ObjectMix, room_size, adm_builder, object_index):
fps = scene.render.fps
frame_start = scene.frame_start
frame_end = scene.frame_end
block_formats = sound_object.adm_block_formats(room_size=room_size)
created = adm_builder.create_item_objects(track_index=object_index,
name=sound_object.object_name,
block_formats=block_formats)
created.audio_object.start = Fraction(frame_start, fps)
created.audio_object.duration = Fraction(frame_end - frame_start, fps)
created.track_uid.sampleRate = sound_object.sample_rate
created.track_uid.bitDepth = sound_object.bits_per_sample
def adm_for_scene(scene, sound_objects: List[ObjectMix], room_size):
adm_builder = ADMBuilder()
frame_start = scene.frame_start
frame_end = scene.frame_end
fps = scene.render.fps
adm_builder.create_programme(audioProgrammeName=scene.name,
start=Fraction(frame_start, fps),
end=Fraction(frame_end, fps))
adm_builder.create_content(audioContentName="Objects")
for object_index, sound_object in enumerate(sound_objects):
adm_for_object(scene, sound_object, room_size, adm_builder, object_index)
adm = adm_builder.adm
generate_ids(adm)
chna = ChnaChunk()
adm_chna.populate_chna_chunk(chna, adm)
return adm_to_xml(adm), chna
def bext_data(scene, sample_rate, room_size):
description = "SCENE={};ROOM_SIZE={}\n".format(scene.name, room_size).encode("ascii")
originator_name = "Blender {}".format(bpy.app.version_string).encode("ascii")
originator_ref = uuid.uuid1().hex.encode("ascii")
date10 = strftime("%Y-%m-%d").encode("ascii")
time8 = strftime("%H:%M:%S").encode("ascii")
timeref = int(float(scene.frame_start) * sample_rate / float(scene.render.fps))
version = 0
umid = b"\0" * 64
pad = b"\0" * 190
data = struct.pack("<256s32s32s10s8sQH64s190s", description, originator_name,
originator_ref, date10, time8, timeref, version, umid, pad)
return data
def attach_outfile_metadata(out_format, outfile, room_size, scene, sound_objects):
adm, chna = adm_for_scene(scene, sound_objects, room_size=room_size)
outfile.axml = lxml.etree.tostring(adm, pretty_print=True)
outfile.chna = chna
outfile.bext = bext_data(scene, out_format.sampleRate, room_size=room_size)
def write_outfile_audio_data(outfile, shortest_file, sound_objects):
READ_BLOCK = 1024
cursor = 0
# Not sure if this is necessary but lets do it
for obj in sound_objects:
obj.mixdown_reader.seek(0)
while True:
remainder = shortest_file - cursor
to_read = min(READ_BLOCK, remainder)
if to_read == 0:
break
buffer = numpy.zeros((to_read, len(sound_objects)))
for i, sound_object in enumerate(sound_objects):
buffer[:, i] = sound_object.mixdown_reader.read(to_read)[:, 0]
outfile.write(buffer)
cursor = cursor + to_read
def write_muxed_wav(mix_pool: ObjectMixPool, scene, out_format, room_size, outfile, shortest_file):
sound_objects = mix_pool.object_mixes
attach_outfile_metadata(out_format, outfile, room_size, scene, sound_objects)
write_outfile_audio_data(outfile, shortest_file, sound_objects)
def mux_adm_from_object_mix_pool(scene, mix_pool: ObjectMixPool, output_filename, room_size=1.):
object_count = len(mix_pool.object_mixes)
assert object_count > 0
out_format = FormatInfoChunk(channelCount=object_count,
sampleRate=scene.render.ffmpeg.audio_mixrate,
bitsPerSample=24)
with openBw64(output_filename, 'w', formatInfo=out_format) as outfile:
write_muxed_wav(mix_pool, scene, out_format, room_size,
outfile, mix_pool.shortest_file_length)
def print_partition_results(object_groups, sound_sources, too_far_speakers):
print("Will create {} objects for {} sources, ignoring {} sources".format(
len(object_groups), len(sound_sources), len(too_far_speakers)))
for i, group in enumerate(object_groups):
print("Object Group %i" % i)
for source in group:
print(" - %s" % source.name)
def partition_sounds_to_objects(scene, max_objects):
sound_sources = all_speakers(scene)
if len(sound_sources) == 0:
return []
object_groups = group_speakers(sound_sources, scene)
too_far_speakers = []
if len(object_groups) > max_objects:
too_far_speakers = object_groups[max_objects:]
object_groups = object_groups[0:max_objects]
print_partition_results(object_groups, sound_sources, too_far_speakers)
return object_groups, too_far_speakers
def generate_adm(context: bpy.types.Context, filepath: str, room_size: float, max_objects: int):
scene = context.scene
object_groups, _ = partition_sounds_to_objects(scene, max_objects)
if len(object_groups) == 0:
return {'FINISHED'}
mix_groups = object_mixes_from_source_groups(object_groups,
scene=scene,
base_dir=dirname(filepath))
with ObjectMixPool(object_mixes=mix_groups) as pool:
mux_adm_from_object_mix_pool(scene, mix_pool=pool,
output_filename=filepath,
room_size=room_size)
print("Finished muxing ADM")
print("generate_adm exiting")
return {'FINISHED'}

115
intern/geom_utils.py Normal file
View File

@@ -0,0 +1,115 @@
import sys
from math import sqrt
import bpy
import numpy
from numpy.linalg import norm
from mathutils import Vector, Quaternion
class FrameInterval:
def __init__(self, start_frame, end_frame):
self.start_frame = int(start_frame)
self.end_frame = int(end_frame)
def overlaps(self, other : 'FrameInterval') -> bool:
return self.start_frame <= other.start_frame <= self.end_frame or \
other.start_frame <= self.start_frame <= other.end_frame
def compute_relative_vector(camera: bpy.types.Camera, target: bpy.types.Object):
"""
Return a vector from `camera` to `target` in the camera's coordinate space.
The camera's lens is assumed to be norm to the ZX plane.
"""
cam_loc, cam_rot, _ = camera.matrix_world.decompose()
target_loc, _, _ = target.matrix_world.decompose()
relative_vector = target_loc - cam_loc
rotation = cam_rot.to_matrix().transposed()
relative_vector.rotate(rotation)
# The camera's worldvector is norm to the horizon, we want a vector
# down the barrel.
camera_correction = Quaternion( ( sqrt(2.) / 2. , sqrt(2.) / 2. , 0. , 0.) )
relative_vector.rotate(camera_correction)
return relative_vector
def room_norm_vector(vec, room_size=1.) -> Vector:
"""
The Room is tearing me apart, Lisa.
The room is a cube with the camera at its center. We use a chebyshev normalization
to convert a vector in world or camera space into a vector the represents the projection
of that vector onto the room's walls. The Room Vector is the immediate the X, Y and Z
coordinate of the corresponding ADM Block Format source object position.
The Pro Tools/Dolby Atmos workflow I am targeting uses "Room Centric" panner coordinates
("cartesian allocentric coordinates" in ADM speak) and this process seems to yield good
results.
I also experimented with using normalized camera frame coordinates from the
bpy_extras.object_utils.world_to_camera_view method and this gives very good results as
long as the object is on-screen; coordinates for objects off the screen are unusable.
In the future it would be worth exploring wether there's a way to produce ADM
coordinates that are "Screen-accurate" while the object is on-screen, but still gives
sensible results when the object is off-screen as well.
"""
chebyshev = norm(vec, ord=numpy.inf)
if chebyshev < room_size:
return vec / room_size
else:
return vec / chebyshev
def closest_approach_to_camera(scene, speaker_object) -> (float, int):
"""
The distance and frame number of `speaker_object`s closest point to
the scene's camera.
(Works for any object, not just speakers.)
"""
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 speaker_active_time_range(speaker) -> FrameInterval:
"""
The time range of the first strip, of the first NLA Track of this speaker.
"""
start, end = 0xffffffff, -0xffffffff
for track in speaker.animation_data.nla_tracks:
for strip in track.strips:
if strip.frame_start < start:
start = strip.frame_start
if strip.frame_end > end:
end = strip.frame_end
return FrameInterval(start_frame=start, end_frame=end)
def speakers_by_min_distance(scene, speakers):
def min_distance(speaker):
return closest_approach_to_camera(scene, speaker)[0]
return sorted(speakers, key=(lambda spk: min_distance(spk)))
def speakers_by_start_time(speaker_objs):
return sorted(speaker_objs, key=(lambda spk: speaker_active_time_range(spk).start_frame))

164
intern/object_mix.py Normal file
View File

@@ -0,0 +1,164 @@
import os
import bpy
from contextlib import contextmanager
from fractions import Fraction
from typing import List
from ear.fileio.adm.elements import ObjectCartesianPosition, JumpPosition, AudioBlockFormatObjects
from ear.fileio.bw64 import Bw64Reader
from .geom_utils import speaker_active_time_range, compute_relative_vector, room_norm_vector
from .speaker_utils import solo_speakers, unmute_all_speakers
@contextmanager
def adm_object_rendering_context(scene: bpy.types.Scene):
old_ff = scene.render.image_settings.file_format
old_codec = scene.render.ffmpeg.audio_codec
old_chans = scene.render.ffmpeg.audio_channels
scene = bpy.context.scene
scene.render.image_settings.file_format = 'FFMPEG'
scene.render.ffmpeg.audio_codec = 'PCM'
scene.render.ffmpeg.audio_channels = 'MONO'
yield scene
scene.render.image_settings.file_format = old_ff
scene.render.ffmpeg.audio_codec = old_codec
scene.render.ffmpeg.audio_channels = old_chans
class ObjectMix:
def __init__(self, sources: List[bpy.types.Speaker],
scene: bpy.types.Scene, base_dir: str):
self.sources = sources
self.intermediate_filename = None
self.base_dir = base_dir
self.scene = scene
self._mixdown_file_handle = None
self._mixdown_reader = None
@property
def frame_start(self):
return self.scene.frame_start
@property
def frame_end(self):
return self.scene.frame_end
@property
def sample_rate(self):
return self.mixdown_reader.sampleRate
@property
def bits_per_sample(self):
return self.mixdown_reader.bitdepth
@property
def mixdown_reader(self) -> Bw64Reader:
if self._mixdown_reader is None:
self._mixdown_reader = Bw64Reader(self.mixdown_file_handle)
return self._mixdown_reader
@property
def mixdown_file_handle(self):
if self._mixdown_file_handle is None:
self._mixdown_file_handle = open(self.mixdown_filename, 'rb')
return self._mixdown_file_handle
@property
def mixdown_filename(self):
if self.intermediate_filename is None:
self.mixdown()
return self.intermediate_filename
@property
def object_name(self):
return self.sources[0].name
def mixdown(self):
with adm_object_rendering_context(self.scene) as scene:
solo_speakers(scene, self.sources)
scene_name = bpy.path.clean_name(scene.name)
speaker_name = bpy.path.clean_name(self.object_name)
self.intermediate_filename = os.path.join(self.base_dir, "%s_%s.wav" % (scene_name, speaker_name))
bpy.ops.sound.mixdown(filepath=self.intermediate_filename,
container='WAV', codec='PCM', format='S24')
print("Created mixdown named {}".format(self.intermediate_filename))
unmute_all_speakers(scene)
def adm_block_formats(self, room_size=1.):
fps = self.scene.render.fps
block_formats = []
for speaker_obj in self.sources:
speaker_interval = speaker_active_time_range(speaker_obj)
for frame in range(speaker_interval.start_frame, speaker_interval.end_frame + 1):
self.scene.frame_set(frame)
relative_vector = compute_relative_vector(camera=self.scene.camera, target=speaker_obj)
norm_vec = room_norm_vector(relative_vector, room_size=room_size)
pos = ObjectCartesianPosition(X=norm_vec.x, Y=norm_vec.y, Z=norm_vec.z)
if len(block_formats) == 0 or pos != block_formats[-1].position:
jp = JumpPosition(flag=True, interpolationLength=Fraction(1, fps * 2))
block = AudioBlockFormatObjects(position=pos,
rtime=Fraction(frame, fps),
duration=Fraction(1, fps),
cartesian=True,
jumpPosition=jp)
block_formats.append(block)
else:
block_formats[-1].duration = block_formats[-1].duration + Fraction(1, fps)
return block_formats
def rm_mixdown(self):
if self._mixdown_reader is not None:
self._mixdown_reader = None
if self._mixdown_file_handle is not None:
self._mixdown_file_handle.close()
self._mixdown_file_handle = None
if self.intermediate_filename is not None:
os.remove(self.intermediate_filename)
self.intermediate_filename = None
class ObjectMixPool:
def __init__(self, object_mixes: List[ObjectMix]):
self.object_mixes = object_mixes
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
for mix in self.object_mixes:
mix.rm_mixdown()
@property
def shortest_file_length(self):
lengths = map(lambda f: len(f.mixdown_reader), self.object_mixes)
return min(lengths)
def object_mixes_from_source_groups(groups: List[List[bpy.types.Speaker]], scene, base_dir):
mixes = []
for group in groups:
mixes.append(ObjectMix(sources=group, scene=scene, base_dir=base_dir))
return mixes

48
intern/soundbank.py Normal file
View File

@@ -0,0 +1,48 @@
import bpy
import numpy
from typing import List
from random import choice
from aud import Sound
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:
if len(self.sounds()) == 0:
return None
else:
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]
else:
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

18
intern/speaker_utils.py Normal file
View File

@@ -0,0 +1,18 @@
def all_speakers(scene):
return [obj for obj in scene.objects if obj.type == 'SPEAKER']
def solo_speakers(scene, solo_group):
for speaker in all_speakers(scene):
if speaker in solo_group:
speaker.data.muted = False
else:
speaker.data.muted = True
speaker.data.update_tag()
def unmute_all_speakers(scene):
for speaker in all_speakers(scene):
speaker.data.muted = False
speaker.data.update_tag()