Compare commits

...

3 Commits

Author SHA1 Message Date
a85d796f07 Cleanup and modernization 2025-11-05 20:52:22 -08:00
5e4fae092a Modernizations 2025-11-05 20:41:11 -08:00
4cbab3cd38 Modernization 2025-11-05 20:40:42 -08:00
3 changed files with 65 additions and 44 deletions

View File

@@ -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))

View File

@@ -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:

View File

@@ -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)