Compare commits
3 Commits
08ad33e27d
...
a85d796f07
| Author | SHA1 | Date | |
|---|---|---|---|
| a85d796f07 | |||
| 5e4fae092a | |||
| 4cbab3cd38 |
@@ -19,7 +19,8 @@ class FrameInterval:
|
|||||||
other.start_frame <= self.start_frame <= other.end_frame
|
other.start_frame <= self.start_frame <= other.end_frame
|
||||||
|
|
||||||
|
|
||||||
def compute_relative_vector(camera: bpy.types.Camera, target: bpy.types.Object):
|
def compute_relative_vector(camera: bpy.types.Object,
|
||||||
|
target: bpy.types.Object):
|
||||||
"""
|
"""
|
||||||
Return a vector from `camera` to `target` in the camera's coordinate space.
|
Return a vector from `camera` to `target` in the camera's coordinate space.
|
||||||
|
|
||||||
@@ -44,22 +45,24 @@ def room_norm_vector(vec, room_size=1.) -> Vector:
|
|||||||
"""
|
"""
|
||||||
The Room is tearing me apart, Lisa.
|
The Room is tearing me apart, Lisa.
|
||||||
|
|
||||||
The room is a cube with the camera at its center. We use a chebyshev normalization
|
The room is a cube with the camera at its center. We use a chebyshev
|
||||||
to convert a vector in world or camera space into a vector the represents the projection
|
normalization to convert a vector in world or camera space into a vector
|
||||||
of that vector onto the room's walls. The Room Vector is the immediate the X, Y and Z
|
the represents the projection of that vector onto the room's walls. The
|
||||||
coordinate of the corresponding ADM Block Format source object position.
|
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
|
The Pro Tools/Dolby Atmos workflow I am targeting uses "Room Centric"
|
||||||
("cartesian allocentric coordinates" in ADM speak) and this process seems to yield good
|
panner coordinates ("cartesian allocentric coordinates" in ADM speak) and
|
||||||
results.
|
this process seems to yield good results.
|
||||||
|
|
||||||
I also experimented with using normalized camera frame coordinates from the
|
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
|
bpy_extras.object_utils.world_to_camera_view method and this gives very
|
||||||
long as the object is on-screen; coordinates for objects off the screen are unusable.
|
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
|
In the future it would be worth exploring wether there's a way to produce
|
||||||
coordinates that are "Screen-accurate" while the object is on-screen, but still gives
|
ADM coordinates that are "Screen-accurate" while the object is on-screen,
|
||||||
sensible results when the object is off-screen as well.
|
but still gives sensible results when the object is off-screen as well.
|
||||||
"""
|
"""
|
||||||
chebyshev = norm(vec, ord=numpy.inf)
|
chebyshev = norm(vec, ord=numpy.inf)
|
||||||
if chebyshev < room_size:
|
if chebyshev < room_size:
|
||||||
@@ -70,8 +73,8 @@ def room_norm_vector(vec, room_size=1.) -> Vector:
|
|||||||
|
|
||||||
def closest_approach_to_camera(scene, speaker_object) -> tuple[float, int]:
|
def closest_approach_to_camera(scene, speaker_object) -> tuple[float, int]:
|
||||||
"""
|
"""
|
||||||
The distance and frame number of `speaker_object`s closest point to
|
The distance and frame number of `speaker_object`s closest point to the
|
||||||
the scene's camera.
|
scene's camera.
|
||||||
|
|
||||||
(Works for any object, not just speakers.)
|
(Works for any object, not just speakers.)
|
||||||
"""
|
"""
|
||||||
@@ -118,4 +121,5 @@ def speakers_by_min_distance(scene, speakers):
|
|||||||
|
|
||||||
|
|
||||||
def speakers_by_start_time(speaker_objs):
|
def speakers_by_start_time(speaker_objs):
|
||||||
return sorted(speaker_objs, key=(lambda spk: speaker_active_time_range(spk).start_frame))
|
return sorted(speaker_objs,
|
||||||
|
key=(lambda spk: speaker_active_time_range(spk).start_frame))
|
||||||
|
|||||||
@@ -4,10 +4,12 @@ from contextlib import contextmanager
|
|||||||
from fractions import Fraction
|
from fractions import Fraction
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from ear.fileio.adm.elements import ObjectCartesianPosition, JumpPosition, AudioBlockFormatObjects
|
from ear.fileio.adm.elements import (ObjectCartesianPosition, JumpPosition,
|
||||||
|
AudioBlockFormatObjects)
|
||||||
from ear.fileio.bw64 import Bw64Reader
|
from ear.fileio.bw64 import Bw64Reader
|
||||||
|
|
||||||
from .geom_utils import speaker_active_time_range, compute_relative_vector, room_norm_vector
|
from .geom_utils import (speaker_active_time_range, compute_relative_vector,
|
||||||
|
room_norm_vector)
|
||||||
from .speaker_utils import solo_speakers, unmute_all_speakers
|
from .speaker_utils import solo_speakers, unmute_all_speakers
|
||||||
|
|
||||||
|
|
||||||
@@ -89,12 +91,14 @@ class ObjectMix:
|
|||||||
scene_name = bpy.path.clean_name(scene.name)
|
scene_name = bpy.path.clean_name(scene.name)
|
||||||
speaker_name = bpy.path.clean_name(self.object_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))
|
self.intermediate_filename = os.path.join(
|
||||||
|
self.base_dir, "%s_%s.wav" % (scene_name, speaker_name))
|
||||||
|
|
||||||
bpy.ops.sound.mixdown(filepath=self.intermediate_filename,
|
bpy.ops.sound.mixdown(filepath=self.intermediate_filename,
|
||||||
container='WAV', codec='PCM', format='S24')
|
container='WAV', codec='PCM', format='S24')
|
||||||
|
|
||||||
print("Created mixdown named {}".format(self.intermediate_filename))
|
print("Created mixdown named {}"
|
||||||
|
.format(self.intermediate_filename))
|
||||||
|
|
||||||
unmute_all_speakers(scene)
|
unmute_all_speakers(scene)
|
||||||
|
|
||||||
@@ -104,16 +108,24 @@ class ObjectMix:
|
|||||||
|
|
||||||
for speaker_obj in self.sources:
|
for speaker_obj in self.sources:
|
||||||
speaker_interval = speaker_active_time_range(speaker_obj)
|
speaker_interval = speaker_active_time_range(speaker_obj)
|
||||||
for frame in range(speaker_interval.start_frame, speaker_interval.end_frame + 1):
|
for frame in range(speaker_interval.start_frame,
|
||||||
|
speaker_interval.end_frame + 1):
|
||||||
|
|
||||||
|
assert self.scene.camera
|
||||||
self.scene.frame_set(frame)
|
self.scene.frame_set(frame)
|
||||||
relative_vector = compute_relative_vector(camera=self.scene.camera, target=speaker_obj)
|
relative_vector = compute_relative_vector(
|
||||||
|
camera=self.scene.camera,
|
||||||
|
target=speaker_obj)
|
||||||
|
|
||||||
norm_vec = room_norm_vector(relative_vector, room_size=room_size)
|
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)
|
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:
|
if len(block_formats) == 0 or pos != block_formats[-1].position:
|
||||||
jp = JumpPosition(flag=True, interpolationLength=Fraction(1, fps * 2))
|
jp = JumpPosition(
|
||||||
|
flag=True, interpolationLength=Fraction(1, fps * 2))
|
||||||
block = AudioBlockFormatObjects(position=pos,
|
block = AudioBlockFormatObjects(position=pos,
|
||||||
rtime=Fraction(frame, fps),
|
rtime=Fraction(frame, fps),
|
||||||
duration=Fraction(1, fps),
|
duration=Fraction(1, fps),
|
||||||
@@ -122,7 +134,8 @@ class ObjectMix:
|
|||||||
|
|
||||||
block_formats.append(block)
|
block_formats.append(block)
|
||||||
else:
|
else:
|
||||||
block_formats[-1].duration = block_formats[-1].duration + Fraction(1, fps)
|
block_formats[-1].duration = block_formats[-1].duration + \
|
||||||
|
Fraction(1, fps)
|
||||||
|
|
||||||
return block_formats
|
return block_formats
|
||||||
|
|
||||||
@@ -157,7 +170,7 @@ class ObjectMixPool:
|
|||||||
return min(lengths)
|
return min(lengths)
|
||||||
|
|
||||||
|
|
||||||
def object_mixes_from_source_groups(groups: List[List[bpy.types.Object]],
|
def object_mixes_from_source_groups(groups: List[List[bpy.types.Object]],
|
||||||
scene: bpy.types.Scene, base_dir: str):
|
scene: bpy.types.Scene, base_dir: str):
|
||||||
mixes = []
|
mixes = []
|
||||||
for group in groups:
|
for group in groups:
|
||||||
|
|||||||
@@ -1,33 +1,36 @@
|
|||||||
from bpy_extras.io_utils import ExportHelper
|
from bpy_extras.io_utils import ExportHelper
|
||||||
from bpy.props import StringProperty, BoolProperty, EnumProperty, FloatProperty, IntProperty
|
from bpy.props import StringProperty, BoolProperty, FloatProperty, IntProperty
|
||||||
from bpy.types import Operator
|
from bpy.types import Operator
|
||||||
|
|
||||||
from .intern.generate_adm import generate_adm
|
from .intern.generate_adm import generate_adm
|
||||||
|
|
||||||
class ADMWaveExport(Operator, ExportHelper):
|
|
||||||
"""Export a Broadcast-WAV audio file with each speaker encoded as an ADM object"""
|
|
||||||
bl_idname = "export.adm_wave_file" # important since its how bpy.ops.import_test.some_data is constructed
|
|
||||||
bl_label = "Export ADM Wave File"
|
|
||||||
|
|
||||||
# ExportHelper mixin class uses this
|
class ADMWaveExport(Operator, ExportHelper):
|
||||||
|
"""
|
||||||
|
Export a Broadcast-WAV audio file with each speaker encoded as an ADM
|
||||||
|
object
|
||||||
|
"""
|
||||||
|
bl_idname = "export.adm_wave_file" # important since its how
|
||||||
|
bl_label = "Export ADM Wave File"
|
||||||
|
filepath: str
|
||||||
filename_ext = ".wav"
|
filename_ext = ".wav"
|
||||||
|
|
||||||
filter_glob: StringProperty(
|
filter_glob = StringProperty(
|
||||||
default="*.wav",
|
default="*.wav",
|
||||||
options={'HIDDEN'},
|
options={'HIDDEN'},
|
||||||
maxlen=255, # Max internal buffer length, longer would be clamped.
|
maxlen=255, # Max internal buffer length, longer would be clamped.
|
||||||
)
|
)
|
||||||
|
|
||||||
room_size: FloatProperty(
|
room_size = FloatProperty(
|
||||||
default=1.0,
|
default=1.0,
|
||||||
name="Room Size",
|
name="Room Size",
|
||||||
description="Distance from the lens to the front room boundary",
|
description="Distance from the lens to the front room boundary",
|
||||||
min=0.001,
|
min=0.001,
|
||||||
step=1.,
|
step=1,
|
||||||
unit='LENGTH'
|
unit='LENGTH'
|
||||||
)
|
)
|
||||||
|
|
||||||
max_objects: IntProperty(
|
max_objects = IntProperty(
|
||||||
name="Max Objects",
|
name="Max Objects",
|
||||||
description="Maximum number of object tracks to create",
|
description="Maximum number of object tracks to create",
|
||||||
default=24,
|
default=24,
|
||||||
@@ -35,13 +38,14 @@ class ADMWaveExport(Operator, ExportHelper):
|
|||||||
max=118
|
max=118
|
||||||
)
|
)
|
||||||
|
|
||||||
create_bed: BoolProperty(
|
create_bed = BoolProperty(
|
||||||
name="Create 7.1 Bed",
|
name="Create 7.1 Bed",
|
||||||
description="Create a bed for all sounds not included on object tracks",
|
description="Create a bed for all sounds not included on object "
|
||||||
|
"tracks",
|
||||||
default=False,
|
default=False,
|
||||||
options={'HIDDEN'}
|
options={'HIDDEN'}
|
||||||
)
|
)
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context) -> set:
|
||||||
return generate_adm(context, self.filepath, self.room_size, self.max_objects)
|
return generate_adm(context, self.filepath, self.room_size,
|
||||||
|
self.max_objects)
|
||||||
|
|||||||
Reference in New Issue
Block a user