From e73b0b7b207a9ede9d5a1f7671c0e60123f71b30 Mon Sep 17 00:00:00 2001 From: Jamie Hardt Date: Wed, 16 Sep 2020 18:32:50 -0700 Subject: [PATCH] First commit --- .gitignore | 2 + operator_add_speakers_to_objects.py | 319 ++++++++++++++++++++++ operator_adm_export.py | 395 ++++++++++++++++++++++++++++ operator_wav_import.py | 106 ++++++++ 4 files changed, 822 insertions(+) create mode 100644 .gitignore create mode 100644 operator_add_speakers_to_objects.py create mode 100644 operator_adm_export.py create mode 100644 operator_wav_import.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cceaafc --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.pyc +.vscode/* diff --git a/operator_add_speakers_to_objects.py b/operator_add_speakers_to_objects.py new file mode 100644 index 0000000..eb2c272 --- /dev/null +++ b/operator_add_speakers_to_objects.py @@ -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() + diff --git a/operator_adm_export.py b/operator_adm_export.py new file mode 100644 index 0000000..889f38b --- /dev/null +++ b/operator_adm_export.py @@ -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() + diff --git a/operator_wav_import.py b/operator_wav_import.py new file mode 100644 index 0000000..5a0326a --- /dev/null +++ b/operator_wav_import.py @@ -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')