First commit
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
*.pyc
|
||||||
|
.vscode/*
|
||||||
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()
|
||||||
|
|
||||||
395
operator_adm_export.py
Normal file
395
operator_adm_export.py
Normal file
@@ -0,0 +1,395 @@
|
|||||||
|
import sys
|
||||||
|
import bpy
|
||||||
|
import os
|
||||||
|
|
||||||
|
from ear.fileio.utils import openBw64
|
||||||
|
|
||||||
|
from ear.fileio.bw64.utils import interleave
|
||||||
|
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.elements.block_formats import (AudioBlockFormatObjects, JumpPosition)
|
||||||
|
from ear.fileio.adm.elements.geom import ObjectCartesianPosition
|
||||||
|
from ear.fileio.adm.builder import (ADMBuilder, TypeDefinition)
|
||||||
|
from ear.fileio.adm.generate_ids import generate_ids
|
||||||
|
|
||||||
|
|
||||||
|
import lxml
|
||||||
|
import uuid
|
||||||
|
from fractions import Fraction
|
||||||
|
import struct
|
||||||
|
|
||||||
|
import numpy
|
||||||
|
from numpy.linalg import norm
|
||||||
|
from mathutils import Quaternion, Vector
|
||||||
|
|
||||||
|
from time import strftime
|
||||||
|
from math import sqrt
|
||||||
|
|
||||||
|
bl_info = {
|
||||||
|
"name": "Export ADM Broadcast-WAV File",
|
||||||
|
"description": "Export a Broadcast-WAV with each speaker as an ADM object",
|
||||||
|
"author": "Jamie Hardt",
|
||||||
|
"version": (0, 22),
|
||||||
|
"warning": "Requires `ear` EBU ADM Renderer package to be installed",
|
||||||
|
"blender": (2, 90, 0),
|
||||||
|
"category": "Import-Export",
|
||||||
|
}
|
||||||
|
|
||||||
|
def compute_relative_vector(camera: bpy.types.Camera, object: bpy.types.Object):
|
||||||
|
"""
|
||||||
|
Return a vector from `camera` to `object` 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()
|
||||||
|
obj_loc, _, _ = object.matrix_world.decompose()
|
||||||
|
relative_vector = obj_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.):
|
||||||
|
"""
|
||||||
|
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 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.
|
||||||
|
"""
|
||||||
|
chebyshev = norm(vec, ord=numpy.inf)
|
||||||
|
if chebyshev < room_size:
|
||||||
|
return vec / room_size
|
||||||
|
else:
|
||||||
|
return vec / chebyshev
|
||||||
|
|
||||||
|
|
||||||
|
def speaker_active_time_range(speaker):
|
||||||
|
"""
|
||||||
|
The time range this speaker must control in order to sound right.
|
||||||
|
|
||||||
|
At this time this is assuming the str
|
||||||
|
"""
|
||||||
|
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 int(start), int(end)
|
||||||
|
|
||||||
|
|
||||||
|
def speakers_by_start_time(speaker_objs):
|
||||||
|
return sorted(speaker_objs, key=(lambda spk: speaker_active_time_range(spk)[0]))
|
||||||
|
|
||||||
|
|
||||||
|
def group_speakers(speaker_objs):
|
||||||
|
def group_speakers_impl1(bag):
|
||||||
|
"Returns a useable group and the remainder"
|
||||||
|
leftover = []
|
||||||
|
this_group = []
|
||||||
|
boundary = -0xffffffff
|
||||||
|
for speaker in bag:
|
||||||
|
start, end = speaker_active_time_range(speaker)
|
||||||
|
if start > boundary:
|
||||||
|
this_group.append(speaker)
|
||||||
|
boundary = end
|
||||||
|
else:
|
||||||
|
leftover.append(speaker)
|
||||||
|
|
||||||
|
return (this_group, leftover)
|
||||||
|
|
||||||
|
groups = []
|
||||||
|
remaining = speaker_objs
|
||||||
|
while len(remaining) > 0:
|
||||||
|
results = group_speakers_impl1(remaining)
|
||||||
|
groups.append(results[0])
|
||||||
|
remaining = results[1]
|
||||||
|
|
||||||
|
print("Will group {} sources into {} objects".format(len(speaker_objs), len(groups)))
|
||||||
|
return groups
|
||||||
|
|
||||||
|
|
||||||
|
def adm_block_formats_for_speakers(scene, speaker_objs, room_size=1.):
|
||||||
|
|
||||||
|
block_formats = []
|
||||||
|
|
||||||
|
# frame_start = start_frame or scene.frame_start
|
||||||
|
# frame_end = end_frame or scene.frame_end
|
||||||
|
fps = scene.render.fps
|
||||||
|
|
||||||
|
for speaker_obj in speakers_by_start_time(speaker_objs):
|
||||||
|
speaker_start, speaker_end = speaker_active_time_range(speaker_obj)
|
||||||
|
for frame in range(speaker_start, speaker_end + 1):
|
||||||
|
scene.frame_set(frame)
|
||||||
|
relative_vector = compute_relative_vector(camera=scene.camera,
|
||||||
|
object=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 adm_data_for_scene(scene, speaker_objs_lists, wav_format, room_size):
|
||||||
|
|
||||||
|
b = ADMBuilder()
|
||||||
|
|
||||||
|
frame_start = scene.frame_start
|
||||||
|
frame_end = scene.frame_end
|
||||||
|
fps = scene.render.fps
|
||||||
|
|
||||||
|
b.create_programme(audioProgrammeName=scene.name,
|
||||||
|
start=Fraction(frame_start ,fps),
|
||||||
|
end=Fraction(frame_end, fps) )
|
||||||
|
|
||||||
|
b.create_content(audioContentName="Objects")
|
||||||
|
|
||||||
|
for i, speakers_this_mixdown in enumerate(speaker_objs_lists):
|
||||||
|
block_formats = adm_block_formats_for_speakers(scene, speakers_this_mixdown,
|
||||||
|
room_size=room_size)
|
||||||
|
created = b.create_item_objects(track_index=i,
|
||||||
|
name=speakers_this_mixdown[0].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 = wav_format.sampleRate
|
||||||
|
created.track_uid.bitDepth = wav_format.bitsPerSample
|
||||||
|
|
||||||
|
adm = b.adm
|
||||||
|
|
||||||
|
generate_ids(adm)
|
||||||
|
chna = ChnaChunk()
|
||||||
|
adm_chna.populate_chna_chunk(chna, adm)
|
||||||
|
|
||||||
|
return adm_to_xml(adm), chna
|
||||||
|
|
||||||
|
|
||||||
|
def bext_data(scene, speaker_obj, 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 write_muxed_adm(scene, mixdowns, output_filename=None, room_size=1.):
|
||||||
|
"""
|
||||||
|
mixdowns are a tuple of wave filename, and corresponding speaker object
|
||||||
|
"""
|
||||||
|
object_count = len(mixdowns)
|
||||||
|
assert object_count > 0
|
||||||
|
|
||||||
|
READ_BLOCK=1024
|
||||||
|
out_file = output_filename or os.path.join(os.path.dirname(mixdowns[0][0]),
|
||||||
|
bpy.path.clean_name(scene.name) + ".wav")
|
||||||
|
|
||||||
|
infiles = []
|
||||||
|
shortest_file = 0xFFFFFFFFFFFF
|
||||||
|
for elem in mixdowns:
|
||||||
|
infile = openBw64(elem[0], 'r')
|
||||||
|
infiles.append(infile)
|
||||||
|
if len(infile) < shortest_file:
|
||||||
|
shortest_file = len(infile)
|
||||||
|
|
||||||
|
|
||||||
|
out_format = FormatInfoChunk(channelCount=object_count,
|
||||||
|
sampleRate=infiles[0].sampleRate,
|
||||||
|
bitsPerSample=infiles[0].bitdepth)
|
||||||
|
|
||||||
|
|
||||||
|
with openBw64(out_file, 'w', formatInfo=out_format) as outfile:
|
||||||
|
speakers = list(map(lambda x: x[1], mixdowns))
|
||||||
|
adm, chna = adm_data_for_scene(scene, speakers, out_format, room_size=room_size)
|
||||||
|
outfile.axml = lxml.etree.tostring(adm, pretty_print=True)
|
||||||
|
outfile.chna = chna
|
||||||
|
outfile.bext = bext_data(scene, None, out_format.sampleRate, room_size=room_size)
|
||||||
|
|
||||||
|
cursor = 0
|
||||||
|
while True:
|
||||||
|
remainder = shortest_file - cursor
|
||||||
|
to_read = min(READ_BLOCK, remainder)
|
||||||
|
if to_read == 0:
|
||||||
|
break
|
||||||
|
|
||||||
|
buffer = numpy.zeros((to_read, object_count))
|
||||||
|
for i, infile in enumerate(infiles):
|
||||||
|
buffer[: , i] = infile.read(to_read)[: , 0]
|
||||||
|
|
||||||
|
outfile.write(buffer)
|
||||||
|
cursor = cursor + to_read
|
||||||
|
|
||||||
|
for infile in infiles:
|
||||||
|
infile._buffer.close()
|
||||||
|
|
||||||
|
for elem in mixdowns:
|
||||||
|
os.unlink(elem[0])
|
||||||
|
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
|
||||||
|
def speaker_mixdowns(scene, filepath):
|
||||||
|
basedir = os.path.dirname(filepath)
|
||||||
|
for speaker_group in group_speakers(all_speakers(scene)):
|
||||||
|
solo_speakers(scene, speaker_group)
|
||||||
|
|
||||||
|
scene_name = bpy.path.clean_name(scene.name)
|
||||||
|
speaker_name = bpy.path.clean_name(speaker_group[0].name)
|
||||||
|
|
||||||
|
fn = os.path.join(basedir, "%s_%s.wav" % (scene_name, speaker_name) )
|
||||||
|
bpy.ops.sound.mixdown(filepath=fn, container='WAV', codec='PCM', format='S24')
|
||||||
|
yield (fn, speaker_group)
|
||||||
|
|
||||||
|
|
||||||
|
def save_output_state(context):
|
||||||
|
"""
|
||||||
|
save render settings that we change to produce object WAV files
|
||||||
|
"""
|
||||||
|
ff = context.scene.render.image_settings.file_format
|
||||||
|
codec = context.scene.render.ffmpeg.audio_codec
|
||||||
|
chans = context.scene.render.ffmpeg.audio_channels
|
||||||
|
return (ff, codec, chans)
|
||||||
|
|
||||||
|
|
||||||
|
def restore_output_state(ctx, context):
|
||||||
|
context.scene.render.image_settings.file_format = ctx[0]
|
||||||
|
context.scene.render.ffmpeg.audio_codec = ctx[1]
|
||||||
|
context.scene.render.ffmpeg.audio_channels = ctx[2]
|
||||||
|
|
||||||
|
|
||||||
|
def write_some_data(context, filepath, room_size):
|
||||||
|
ctx = save_output_state(context)
|
||||||
|
|
||||||
|
scene = bpy.context.scene
|
||||||
|
|
||||||
|
scene.render.image_settings.file_format = 'FFMPEG'
|
||||||
|
scene.render.ffmpeg.audio_codec = 'PCM'
|
||||||
|
scene.render.ffmpeg.audio_channels = 'MONO'
|
||||||
|
|
||||||
|
mixdowns = list(speaker_mixdowns(scene, filepath))
|
||||||
|
mixdown_count = len(mixdowns)
|
||||||
|
if mixdown_count == 0:
|
||||||
|
return {'FINISHED'}
|
||||||
|
else:
|
||||||
|
write_muxed_adm(scene, mixdowns, output_filename= filepath, room_size=room_size)
|
||||||
|
|
||||||
|
#cleanup
|
||||||
|
unmute_all_speakers(scene)
|
||||||
|
restore_output_state(ctx, context)
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#########################################################################
|
||||||
|
### BOILERPLATE EXPORTER CODE BELOW
|
||||||
|
|
||||||
|
|
||||||
|
# ExportHelper is a helper class, defines filename and
|
||||||
|
# invoke() function which calls the file selector.
|
||||||
|
from bpy_extras.io_utils import ExportHelper
|
||||||
|
from bpy.props import StringProperty, BoolProperty, EnumProperty, FloatProperty
|
||||||
|
from bpy.types import Operator
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
filename_ext = ".wav"
|
||||||
|
|
||||||
|
filter_glob: StringProperty(
|
||||||
|
default="*.wav",
|
||||||
|
options={'HIDDEN'},
|
||||||
|
maxlen=255, # Max internal buffer length, longer would be clamped.
|
||||||
|
)
|
||||||
|
|
||||||
|
room_size: FloatProperty(
|
||||||
|
default=1.0,
|
||||||
|
name="Room Size",
|
||||||
|
description="Distance from the lens to the front room boundary",
|
||||||
|
min=0.001,
|
||||||
|
step=1.,
|
||||||
|
unit='LENGTH'
|
||||||
|
)
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
return write_some_data(context, self.filepath, self.room_size)
|
||||||
|
|
||||||
|
|
||||||
|
# Only needed if you want to add into a dynamic menu
|
||||||
|
def menu_func_export(self, context):
|
||||||
|
self.layout.operator(ADMWaveExport.bl_idname, text="ADM Broadcast-WAVE (.wav)")
|
||||||
|
|
||||||
|
|
||||||
|
def register():
|
||||||
|
bpy.utils.register_class(ADMWaveExport)
|
||||||
|
bpy.types.TOPBAR_MT_file_export.append(menu_func_export)
|
||||||
|
|
||||||
|
|
||||||
|
def unregister():
|
||||||
|
bpy.utils.unregister_class(ADMWaveExport)
|
||||||
|
bpy.types.TOPBAR_MT_file_export.remove(menu_func_export)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
register()
|
||||||
|
|
||||||
106
operator_wav_import.py
Normal file
106
operator_wav_import.py
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import bpy
|
||||||
|
import os
|
||||||
|
|
||||||
|
bl_info = {
|
||||||
|
"name": "Import WAV Files",
|
||||||
|
"description": "Import WAV files, with options to automatically pack and Fake user",
|
||||||
|
"author": "Jamie Hardt",
|
||||||
|
"version": (0, 20),
|
||||||
|
"blender": (2, 90, 0),
|
||||||
|
"location": "File > Import > WAV Audio Files",
|
||||||
|
"warning": "", # used for warning icon and text in addons panel
|
||||||
|
"support": "COMMUNITY",
|
||||||
|
"category": "Import-Export",
|
||||||
|
}
|
||||||
|
|
||||||
|
def read_some_data(context, filepath, pack, dir, fake):
|
||||||
|
|
||||||
|
def import_one(fp):
|
||||||
|
sound = bpy.data.sounds.load(fp, check_existing=False)
|
||||||
|
if pack:
|
||||||
|
sound.pack()
|
||||||
|
|
||||||
|
if fake:
|
||||||
|
sound.use_fake_user = True
|
||||||
|
|
||||||
|
if dir:
|
||||||
|
the_dir = os.path.dirname(filepath)
|
||||||
|
for child in os.listdir(the_dir):
|
||||||
|
if child.endswith(".wav"):
|
||||||
|
import_one(os.path.join(the_dir, child))
|
||||||
|
|
||||||
|
else:
|
||||||
|
import_one(filepath)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
|
# ImportHelper is a helper class, defines filename and
|
||||||
|
# invoke() function which calls the file selector.
|
||||||
|
from bpy_extras.io_utils import ImportHelper
|
||||||
|
from bpy.props import StringProperty, BoolProperty, EnumProperty
|
||||||
|
from bpy.types import Operator
|
||||||
|
|
||||||
|
|
||||||
|
class ImportWav(Operator, ImportHelper):
|
||||||
|
"""Import WAV audio files"""
|
||||||
|
bl_idname = "import_test.wav_file_batch" # important since its how bpy.ops.import_test.some_data is constructed
|
||||||
|
bl_label = "Import WAV Files"
|
||||||
|
|
||||||
|
# ImportHelper mixin class uses this
|
||||||
|
filename_ext = ".wav"
|
||||||
|
|
||||||
|
filter_glob: StringProperty(
|
||||||
|
default="*.wav",
|
||||||
|
options={'HIDDEN'},
|
||||||
|
maxlen=255, # Max internal buffer length, longer would be clamped.
|
||||||
|
)
|
||||||
|
|
||||||
|
# List of operator properties, the attributes will be assigned
|
||||||
|
# to the class instance from the operator settings before calling.
|
||||||
|
# pack_it_in: BoolProperty(
|
||||||
|
# name="Pack Into .blend File",
|
||||||
|
# description="Embed the audio data into the .blend file",
|
||||||
|
# default=True,
|
||||||
|
# )
|
||||||
|
|
||||||
|
fake: BoolProperty(
|
||||||
|
name="Add Fake User",
|
||||||
|
description="Add the Fake User to each of the sound files",
|
||||||
|
default=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
all_in_directory: BoolProperty(
|
||||||
|
name="All Files in Folder",
|
||||||
|
description="Import every WAV file found in the folder as the selected file",
|
||||||
|
default=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
return read_some_data(context=context, filepath=self.filepath,
|
||||||
|
dir=self.all_in_directory, fake=self.fake,
|
||||||
|
pack=False)
|
||||||
|
|
||||||
|
|
||||||
|
# Only needed if you want to add into a dynamic menu
|
||||||
|
def menu_func_import(self, context):
|
||||||
|
self.layout.operator(ImportWav.bl_idname, text="WAV Audio Files (.wav)")
|
||||||
|
|
||||||
|
|
||||||
|
def register():
|
||||||
|
bpy.utils.register_class(ImportWav)
|
||||||
|
bpy.types.TOPBAR_MT_file_import.append(menu_func_import)
|
||||||
|
|
||||||
|
|
||||||
|
def unregister():
|
||||||
|
bpy.utils.unregister_class(ImportWav)
|
||||||
|
bpy.types.TOPBAR_MT_file_import.remove(menu_func_import)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
register()
|
||||||
|
|
||||||
|
# test call
|
||||||
|
# bpy.ops.import_test.wav_file_batch('INVOKE_DEFAULT')
|
||||||
Reference in New Issue
Block a user