├── .gitignore ├── LICENSE ├── README.md ├── __init__.py ├── analyzer ├── analyzer.py ├── midi_file │ └── __init__.py ├── midi_realtime │ ├── __init__.py │ ├── midi_realtime_inputlist_proxy.py │ ├── midi_realtime_proxy.py │ └── midi_thread.py ├── nonstop_baking.py ├── realtime │ ├── __init__.py │ └── realtime_thread.py ├── recording.py ├── sequence.py ├── shapemodifier │ ├── __init__.py │ ├── greasepencil.py │ ├── lib.py │ ├── uv.py │ ├── vertcolor.py │ └── vertexweight.py ├── spectrogram.py └── video │ ├── __init__.py │ ├── video_to_curve.py │ ├── video_to_gpencil.py │ ├── video_to_mesh.py │ └── video_to_object.py ├── audvis_class.py ├── bge ├── __init__.py ├── bge_common.py ├── bge_midi.py ├── bge_realtime.py └── bge_updater.py ├── blender_manifest.toml ├── build-extension.sh ├── doc ├── _doc.html ├── armature.md ├── bake-drivers.md ├── driver-values.md ├── drivers.md ├── example-objects.md ├── freq-sequencing.md ├── img │ ├── basic-view.png │ ├── curve-anim.gif │ ├── generator.png │ ├── sequence-analyzer.png │ └── shape-modifier.png ├── midi-file.md ├── midi-realtime.md ├── packages-install.md ├── realtime.md ├── scripting.md ├── scripts │ ├── curve.py │ ├── curve2.py │ ├── curve3.py │ ├── empty.py │ ├── gpencil-ekg.py │ ├── gpencil-ekg2.py │ ├── gpencil-from-camera.py │ ├── gpencil-hair.py │ ├── gpencil-spiral.py │ ├── gpencil-worms.py │ ├── image-generator.py │ ├── mesh-grid.py │ ├── mesh-skin.py │ ├── particles.py │ └── suzanne.py ├── sequence.md ├── shape-modifier.md ├── spectrogram.md ├── spread-the-drivers.md ├── upbge.md └── video-capture.md ├── note_calculator.py ├── requirements-nodeps.txt ├── requirements.txt ├── scripting.py ├── switchscenes.py ├── ui ├── __init__.py ├── animation_nodes │ ├── __init__.py │ ├── expression.py │ └── script.py ├── armature_generator │ ├── __init__.py │ └── generate.py ├── bge │ ├── __init__.py │ ├── bge_logic_create.py │ └── bge_register_component.py ├── buttonspanel.py ├── daw_arrangement │ ├── .gitignore │ ├── DawArrangement-blender3_6.blend │ ├── __init__.py │ ├── arrangement.py │ ├── outputs │ │ └── geometrynodes1.py │ └── parser │ │ ├── ableton_color_map.py │ │ ├── als.py │ │ ├── audiofile.py │ │ ├── dawproject.py │ │ └── midi.py ├── drivers_bake │ └── __init__.py ├── force_reload.py ├── generator │ ├── __init__.py │ ├── generate.py │ └── material.py ├── global_settings.py ├── hz_label.py ├── install_lib.py ├── install_ui │ ├── __init__.py │ ├── all.py │ ├── realtime.py │ ├── recording.py │ └── video.py ├── midi │ ├── __init__.py │ ├── file.py │ ├── midi_file_baker.py │ ├── operators │ │ ├── midiFileOpen.py │ │ ├── midiFileRemove.py │ │ └── midiTrackRemove.py │ ├── realtime.py │ └── utils.py ├── partymode │ ├── __init__.py │ ├── audvis-party-workspace.blend │ ├── gizmo.py │ ├── in_window.py │ ├── in_workspace.py │ └── party_panel.py ├── pip_installer.py ├── preferences.py ├── props │ ├── __init__.py │ ├── animationnodes.py │ ├── armaturegenerator.py │ ├── daw_arrangement.py │ ├── lib.py │ ├── midi.py │ ├── obj.py │ ├── party.py │ ├── realtimeprops.py │ ├── scene.py │ ├── sequence.py │ ├── shapemodifier.py │ ├── spectrogram.py │ ├── spreaddrivers.py │ └── valuesaud.py ├── realtime │ ├── __init__.py │ └── recording.py ├── scripttemplates.py ├── sequence.py ├── shapemodifier.py ├── spectrogram │ ├── __init__.py │ ├── bake.py │ └── operators │ │ ├── spectrogramAdd.py │ │ ├── spectrogramDuplicate.py │ │ ├── spectrogramMakeParticles.py │ │ ├── spectrogramRemove.py │ │ └── spectrogramSingleToMulti.py ├── spread_drivers │ ├── __init__.py │ ├── expression_builder.py │ ├── menuitem.py │ └── spread_drivers_ui.py ├── ui_lib.py ├── values │ ├── __init__.py │ └── presets.py └── video │ └── __init__.py └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | doc/bl_app_templates_user/AudVis/startup.blend1 3 | .idea/ 4 | venv/ 5 | blender_autocomplete/ 6 | wheels/ 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Blender AudVis 2 | 3 | AudVis is a Blender 2.8 and higher add-on helping you to build awesome audio visualizations. The main features are Real 4 | Time Analyzer and Sequence Analyzer. 5 | 6 | ## Important links: 7 | 8 | - **[Download](https://github.com/example-sk/audvis/releases)** 9 | - [Direct donation on PayPal](https://www.paypal.com/donate/?business=A8HN8ADXCKJ8Y&no_recurring=0&item_name=Do+you+love+Blender+AudVis?¤cy_code=EUR) 10 | - [BlenderMarket product page](https://www.blendermarket.com/products/audvis) 11 | - [YouTube channel](https://www.youtube.com/channel/UCiJzmdCGdjLc_fpQgZ2fEhQ) 12 | - [blenderartists.org discussion](https://blenderartists.org/t/audvis-audio-visualization-add-on/1183964) 13 | - [Discord](https://discord.gg/bFzVmBGg) 14 | - [Old documentation](https://www.blendermarket.com/products/audvis/docs) 15 | 16 | ## Documentation: 17 | 18 | * [Installing python packages](doc/packages-install.md) 19 | * Analyzers: 20 | - [Sequence Analyzer](doc/sequence.md) 21 | - [Realtime Analyzer](doc/realtime.md) 22 | - [MIDI File](doc/midi-file.md) 23 | - [MIDI Realtime](doc/midi-realtime.md) 24 | * [Driver Values](doc/driver-values.md) 25 | * How to animate things: 26 | - [Using drivers](doc/drivers.md) 27 | - [Shape Modifier](doc/shape-modifier.md) 28 | - [Generate Armature](doc/armature.md) 29 | - [Generate Example Objects](doc/example-objects.md) 30 | - [Scripting](doc/scripting.md) 31 | * [Spectrogram](doc/spectrogram.md) 32 | * [Video Capture](doc/video-capture.md) 33 | * [Bake Drivers](doc/bake-drivers.md) 34 | * [Spread the Drivers](doc/spread-the-drivers.md) 35 | * [UPBGE](doc/upbge.md) 36 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import importlib 3 | import sys 4 | 5 | bl_info = { 6 | "name": "AudVis - audio visualization driver", 7 | "author": "example.sk", 8 | "version": (7, 2, 2), 9 | "blender": (3, 6, 0), 10 | "location": "View 3D > Sidebar > AudVis Tab", 11 | "description": ( 12 | "Tools for awesome audio visualizations. You can use Sequence Analyzer to render a music video, or Realtime Analyzer combined with Party Mode to impress people on your party."), 13 | "wiki_url": "https://example.sk/audvis/", 14 | "category": "Animation"} 15 | 16 | import bpy 17 | from bpy.app.handlers import persistent 18 | 19 | bpy._audvis_module = __package__ 20 | 21 | from . import scripting 22 | from . import ui 23 | from .ui import install_lib 24 | from .audvis_class import AudVis 25 | 26 | classes = ui.classes + scripting.classes 27 | 28 | is_first_load = True 29 | 30 | 31 | def _reload_modules(): 32 | for key in list(sys.modules.keys()): 33 | if key.startswith("audvis"): 34 | importlib.reload(sys.modules[key]) 35 | 36 | 37 | def register2(): 38 | global is_first_load 39 | global audvis 40 | global register_script 41 | global classes 42 | 43 | if is_first_load: 44 | is_first_load = False 45 | else: 46 | for i in range(2): # just to be sure... 47 | _reload_modules() 48 | 49 | audvis = AudVis() 50 | bpy.audvis = audvis 51 | audvis._module_name = __package__ 52 | register_script = audvis.register_script 53 | classes = ui.classes + scripting.classes 54 | for c in classes: 55 | try: 56 | bpy.utils.register_class(c) 57 | except: 58 | pass 59 | 60 | 61 | def unregister2(): 62 | for c in reversed(classes): 63 | try: 64 | bpy.utils.unregister_class(c) 65 | except: 66 | pass 67 | 68 | 69 | @persistent 70 | def load_handler(dummy): 71 | bpy.app.driver_namespace['audvis'] = audvis.driver 72 | if audvis.update_data not in bpy.app.handlers.frame_change_pre: 73 | bpy.app.handlers.frame_change_pre.append(audvis.update_data) 74 | # bpy.app.handlers.frame_change_pre.append(audvis.profiled_update_data) 75 | if save_pre_handler not in bpy.app.handlers.save_pre: 76 | bpy.app.handlers.save_pre.append(save_pre_handler) 77 | ui.on_blendfile_loaded() 78 | 79 | 80 | @persistent 81 | def unload_handler(dummy): 82 | audvis.reload() 83 | 84 | 85 | def save_pre_handler(dummy): 86 | ui.on_blendfile_save() 87 | 88 | 89 | def register(): 90 | register_syspath() 91 | register2() 92 | ui.register() 93 | bpy.app.handlers.load_post.append(load_handler) 94 | bpy.app.handlers.load_pre.append(unload_handler) 95 | register_keymaps() 96 | load_handler(None) 97 | 98 | 99 | def register_syspath(): 100 | path = install_lib.get_libs_path_latest() 101 | if path is None: 102 | return 103 | unregister_syspath() 104 | sys.path.append(path) 105 | 106 | 107 | def unregister_syspath(): 108 | lst = [d for d in sys.path if d.startswith(install_lib.audvis_libs_path)] 109 | for d in lst: 110 | sys.path.remove(d) 111 | 112 | 113 | def unregister(): 114 | global audvis 115 | unregister_syspath() # probably useless 116 | unregister_keymaps() 117 | unregister2() 118 | ui.unregister() 119 | bpy.app.handlers.frame_change_pre.remove(audvis.update_data) 120 | bpy.app.handlers.load_post.remove(load_handler) 121 | bpy.app.handlers.save_pre.remove(save_pre_handler) 122 | bpy.app.driver_namespace.pop('audvis') 123 | audvis = None 124 | bpy.audvis = None 125 | 126 | 127 | ### keymaps 128 | addon_keymaps = [] 129 | 130 | 131 | def register_keymaps(): 132 | wm = bpy.context.window_manager 133 | kc = wm.keyconfigs.addon 134 | if kc: 135 | # region_type in ('WINDOW', 'HEADER', 'CHANNELS', 'TEMPORARY', 'UI', 'TOOLS', 'TOOL_PROPS', 136 | # 'PREVIEW', 'HUD', 'NAVIGATION_BAR', 'EXECUTE', 'FOOTER', 'TOOL_HEADER') 137 | km = kc.keymaps.new(name='3D View', space_type='VIEW_3D') 138 | kmi = km.keymap_items.new("audvis.partymodeclose", "ESC", "PRESS") 139 | addon_keymaps.append((km, kmi)) 140 | 141 | 142 | def unregister_keymaps(): 143 | for km, kmi in addon_keymaps: 144 | km.keymap_items.remove(kmi) 145 | addon_keymaps.clear() 146 | 147 | 148 | if __name__ == "__main__": 149 | register() 150 | -------------------------------------------------------------------------------- /analyzer/midi_file/__init__.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from ...utils import midi_note_to_number 4 | from ..analyzer import Analyzer 5 | 6 | regexp = re.compile('ch([0-9]+)_(n|c)([0-9]+)') 7 | 8 | 9 | class MidiFileAnalyzer(Analyzer): 10 | _thread = None 11 | _last_data = None 12 | _last_data_control = None 13 | 14 | def load(self): 15 | pass 16 | 17 | def _parse_midifile_baked_data(self, midifile, scene): 18 | # custom attributes for sequence can't be animated, at least until 2.93 alpha 19 | result_notes = [] 20 | result_controls = [] 21 | midifile.fix_fps() 22 | fps = scene.render.fps / scene.render.fps_base 23 | endframe = sum([ 24 | midifile.time_length * fps, 25 | midifile.frame_start, 26 | - midifile.animation_offset_start, 27 | - midifile.animation_offset_end 28 | ]) 29 | if midifile.frame_start > scene.frame_current_final or endframe < scene.frame_current_final: 30 | return (result_notes, result_controls) 31 | request_frame = sum([scene.frame_current_final, 32 | -midifile.frame_start, 33 | midifile.animation_offset_start, 34 | ]) 35 | for track in midifile.tracks: 36 | if not track.enable: 37 | continue 38 | common_path = track.path_from_id() 39 | for fcurve in scene.animation_data.action.fcurves: 40 | if fcurve.data_path.startswith(common_path): 41 | tmp = fcurve.data_path.replace(common_path, '').replace('["', '').replace('"]', '') 42 | regexp_result = regexp.match(tmp) 43 | if regexp_result: 44 | channel = regexp_result.group(1) 45 | type = regexp_result.group(2) 46 | note = regexp_result.group(3) 47 | append_to = result_notes 48 | if type == 'c': 49 | append_to = result_controls 50 | append_to.append(( 51 | midifile.name, 52 | track.name, 53 | int(channel), 54 | int(note), 55 | fcurve.evaluate(request_frame) 56 | )) 57 | return (result_notes, result_controls) 58 | 59 | def on_pre_frame(self, scene, frame): 60 | if not scene.audvis.midi_file.enable: 61 | return 62 | if not scene.animation_data or \ 63 | not scene.animation_data.action \ 64 | or len(scene.audvis.midi_file.midi_files) == 0: 65 | self._last_data = None 66 | return 67 | new_data_notes = [] 68 | new_data_controls = [] 69 | for midifile in scene.audvis.midi_file.midi_files: 70 | if midifile.enable: 71 | (result_notes, result_controls) = self._parse_midifile_baked_data(midifile, scene) 72 | new_data_notes += result_notes 73 | new_data_controls += result_controls 74 | self._last_data = new_data_notes 75 | self._last_data_controls = new_data_controls 76 | 77 | def driver(self, low=None, high=None, ch=None, **kwargs): 78 | midi_note = kwargs.get("midi", None) 79 | data = self._last_data 80 | if "midi_control" in kwargs: 81 | midi_note = kwargs.get('midi_control') 82 | data = self._last_data_controls 83 | elif (type(midi_note) is list or type(midi_note) is tuple) and len(midi_note) in [2, 3]: 84 | midi_note = kwargs.get("midi", None) 85 | if (type(midi_note) is list or type(midi_note) is tuple) and len(midi_note) in [2, 3]: 86 | return self._midi_multi_note_driver(low=None, high=None, ch=None, **kwargs) 87 | else: 88 | midi_note = midi_note_to_number(midi_note) 89 | track = kwargs.get("track", None) 90 | file = kwargs.get("file", None) 91 | if data is None: 92 | return 0 93 | if midi_note is None or midi_note >= 127 or midi_note < 0: 94 | return 0 95 | if data is None: 96 | return 0 97 | for item in data: 98 | if item[3] == 0: 99 | continue 100 | if file is not None and file != '' and item[0] != file: 101 | continue 102 | if track is not None and track != '' and item[1] != track: 103 | continue 104 | if ch is not None and ch != "all" and 1 <= int(ch) <= 16 and int(ch) != item[2]: 105 | continue 106 | if midi_note is not None and item[3] != midi_note: 107 | continue 108 | if item[4] != 0.0: 109 | return item[4] 110 | return 0 111 | -------------------------------------------------------------------------------- /analyzer/midi_realtime/__init__.py: -------------------------------------------------------------------------------- 1 | from ...utils import midi_note_to_number 2 | from . import midi_thread 3 | from .midi_thread import _MidiNoteMessage 4 | from ..analyzer import Analyzer 5 | 6 | 7 | class MidiRealtimeAnalyzer(Analyzer): 8 | _thread = None 9 | _last_data = None 10 | _last_data_controls = None 11 | 12 | def get_last_msg(self) -> _MidiNoteMessage: 13 | thread = self._thread 14 | if not thread: 15 | return None 16 | return thread.last_msg 17 | 18 | def load(self): 19 | self._thread = midi_thread.MidiThread(daemon=True) 20 | self._thread.start() 21 | 22 | def restart(self): 23 | if self._thread is not None: 24 | self._thread.restart_all_inputs() 25 | 26 | def stop(self): 27 | pass 28 | 29 | def set_debug_mode(self, enabled): 30 | if self._thread is not None: 31 | self._thread.debug_mode = enabled 32 | 33 | def on_pre_frame(self, scene, frame): 34 | if scene.audvis.midi_realtime.enable: 35 | lst = [] 36 | self._last_data = self._thread.data 37 | self._last_data_controls = self._thread.data_controls 38 | for item in scene.audvis.midi_realtime.inputs: 39 | if item.enable: 40 | lst.append({ 41 | 'custom_name': item.name, 42 | 'device_name': item.input_name, 43 | }) 44 | self._thread.requested_devices = lst 45 | 46 | def driver(self, low=None, high=None, ch=None, **kwargs): 47 | if self._last_data is None and self._last_data_controls is None: 48 | return 0 49 | data = self._last_data 50 | if ch != 'all' and type(ch) == str and ch.isnumeric(): 51 | ch = int(ch) 52 | if "midi_control" in kwargs: 53 | midi_note = kwargs.get('midi_control') 54 | data = self._last_data_controls 55 | elif "midi" in kwargs: 56 | midi_note = kwargs.get("midi", None) 57 | if (type(midi_note) is list or type(midi_note) is tuple) and len(midi_note) in [2, 3]: 58 | return self._midi_multi_note_driver(low=None, high=None, ch=None, **kwargs) 59 | else: 60 | midi_note = midi_note_to_number(kwargs['midi']) 61 | else: 62 | return 0 63 | if midi_note >= 127 or midi_note < 0: 64 | return 0 65 | device_id = kwargs.get('device', None) 66 | device_key = None 67 | for key in data.keys(): 68 | if device_key is not None and key != device_key: 69 | continue 70 | device_data = data[key] 71 | for key2 in device_data.keys(): 72 | if ch is not None and ch != 'all' and key2 != ch: 73 | continue 74 | channel_data = device_data[key2] 75 | for key3 in channel_data.keys(): 76 | if key3 == midi_note and channel_data[key3]: 77 | return channel_data[key3] 78 | return 0 79 | -------------------------------------------------------------------------------- /analyzer/midi_realtime/midi_realtime_inputlist_proxy.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import sys 3 | 4 | parser = argparse.ArgumentParser(description='test') 5 | parser.add_argument('--libs-path', required=False) 6 | 7 | args = parser.parse_args() 8 | if getattr(args, 'libs_path') is not None: 9 | sys.path.append(args.libs_path) 10 | 11 | import mido 12 | mido.set_backend('mido.backends.pygame') 13 | lst = mido.get_input_names() 14 | print("---- delimiter ----") 15 | for input_name in lst: 16 | print(input_name) 17 | sys.stdout.flush() -------------------------------------------------------------------------------- /analyzer/midi_realtime/midi_realtime_proxy.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import sys 3 | 4 | parser = argparse.ArgumentParser(description='test') 5 | parser.add_argument('--libs-path', required=False) 6 | parser.add_argument('--input-name', required=True) 7 | 8 | args = parser.parse_args() 9 | if getattr(args, 'libs_path') is not None: 10 | sys.path.append(args.libs_path) 11 | 12 | import mido 13 | 14 | mido.set_backend('mido.backends.pygame') 15 | 16 | sys.stdout.flush() 17 | 18 | input = mido.open_input(name=args.input_name) 19 | 20 | try: 21 | while True: 22 | msg = input.receive() 23 | if msg.type == 'control_change': 24 | print(msg.channel + 1, "c{}".format(msg.control), msg.value) 25 | sys.stdout.flush() 26 | elif msg.type in ('note_on', 'note_off'): 27 | velocity = msg.velocity if msg.type == 'note_on' else 0 28 | print(msg.channel + 1, msg.note, velocity) 29 | sys.stdout.flush() 30 | 31 | except Exception as e: 32 | print('ERROR', e) 33 | -------------------------------------------------------------------------------- /analyzer/nonstop_baking.py: -------------------------------------------------------------------------------- 1 | def bake(scene): 2 | coll = scene.audvis.nonstop_baking_collection 3 | if coll is not None: 4 | for obj in coll.all_objects: 5 | _obj(obj) 6 | 7 | 8 | def _obj(obj): 9 | animdata = obj.animation_data 10 | if animdata is None: 11 | return 12 | if animdata.drivers is None: 13 | return 14 | for fcurve in animdata.drivers: 15 | if fcurve.driver is None: 16 | continue 17 | if fcurve.driver.type != 'SCRIPTED': 18 | continue 19 | if "audvis(" not in fcurve.driver.expression: 20 | continue 21 | path = fcurve.data_path 22 | index = fcurve.array_index 23 | _insert_keyframe(obj, path, index) 24 | 25 | obj.animation_data 26 | 27 | 28 | def _insert_keyframe(obj, path, index): 29 | try: 30 | obj.keyframe_insert(path, index=index) 31 | except: 32 | obj.keyframe_insert(path) 33 | -------------------------------------------------------------------------------- /analyzer/realtime/__init__.py: -------------------------------------------------------------------------------- 1 | import threading 2 | import time 3 | 4 | import bpy 5 | 6 | from . import realtime_thread 7 | 8 | from ..analyzer import Analyzer 9 | from ..recording import AudVisRecorder 10 | 11 | 12 | class RealtimeAnalyzer(Analyzer): 13 | status = 'init' 14 | supported = None 15 | stream = None 16 | current_stream_name = None 17 | sd = None 18 | thread = None 19 | recorder = AudVisRecorder() 20 | device_name = None 21 | last_chunks = [] 22 | 23 | def load(self): 24 | try: 25 | import sounddevice as sd 26 | self.sd = sd 27 | self.supported = True 28 | except Exception as e: 29 | self.supported = False 30 | # print("AudVis", e) 31 | return 32 | scene = bpy.context.scene 33 | self.on_pre_frame(scene, bpy.context.scene.frame_current_final) 34 | 35 | def recorder_stop(self, folder, format): 36 | self.recorder.stop() 37 | self.stop() 38 | path = self.recorder.save_data(folder, format) 39 | self.restart() 40 | return path 41 | 42 | def kill(self): 43 | if self.thread is not None: 44 | self.thread.kill_me = True 45 | 46 | def get_error(self): 47 | if self.thread is not None: 48 | return self.thread.error 49 | 50 | def stop(self): 51 | self.thread.requested_name = None 52 | self.empty() 53 | 54 | def restart(self): 55 | self._load_stream(bpy.context.scene, True) 56 | 57 | def on_pre_frame(self, scene, frame): 58 | self._load_stream(scene) 59 | self._read_synchronous(scene) 60 | if self.lastdata is not None: 61 | self.calculate_fft() 62 | 63 | def _load_stream(self, scene, force_reload=False): 64 | if self.thread is None: 65 | t = realtime_thread.RealtimeThread(daemon=True) 66 | t.recorder = self.recorder 67 | t.sd = self.sd 68 | self.thread = t 69 | t.start() 70 | if force_reload: 71 | self.thread.force_reload = True 72 | if scene.audvis.realtime_enable: 73 | prefs = bpy.context.preferences.addons[bpy.audvis._module_name].preferences 74 | if False and prefs.realtime_device_use_global: 75 | self.thread.requested_name = prefs.realtime_device 76 | else: 77 | self.thread.requested_name = self.device_name 78 | self.thread.requested_channels = scene.audvis.channels 79 | else: 80 | self.thread.requested_name = None 81 | self.thread.requested_channels = 1 82 | 83 | def _read_synchronous(self, scene): 84 | if self.thread is None: 85 | self.empty() 86 | return 87 | 88 | if self.thread is None or self.thread.callback_data is None: 89 | self.empty() 90 | else: 91 | self.samplerate = self.thread.samplerate 92 | self.lastdata = self.thread.callback_data[-int(scene.audvis.sample_calc * self.samplerate):] 93 | -------------------------------------------------------------------------------- /analyzer/realtime/realtime_thread.py: -------------------------------------------------------------------------------- 1 | import threading 2 | import time 3 | import numpy as np 4 | 5 | from ..recording import AudVisRecorder 6 | 7 | 8 | class RealtimeThread(threading.Thread): 9 | requested_name = None 10 | requested_channels = 1 11 | current_name = None 12 | current_channels = 1 13 | stream = None 14 | samplerate = None 15 | kill_me = False 16 | callback_data = None 17 | sd = None 18 | error = None 19 | force_reload = False 20 | recorder = None # type: AudVisRecorder 21 | 22 | def _restart_if_needed(self): 23 | req = self.requested_name 24 | req_channels = self.requested_channels 25 | cur = self.current_name 26 | cur_channels = self.current_channels 27 | if not self.force_reload and req == cur and req_channels == cur_channels: 28 | return 29 | self.force_reload = False 30 | self.current_name = req 31 | self.current_channels = self.requested_channels 32 | self.callback_data = None 33 | if self.stream and not self.stream.stopped: 34 | self.stream.stop() 35 | if req is None: 36 | if self.callback_data is not None: 37 | self.callback_data = None 38 | return 39 | kwargs = { 40 | 'dtype': 'float32', 41 | 'channels': req_channels, 42 | 'callback': self._stream_cb, 43 | 'blocksize': 256, # TODO: select 44 | 'device': None, 45 | } 46 | i = 0 47 | for device in self.sd.query_devices(): 48 | if device['name'] == req: 49 | kwargs['device'] = i 50 | break 51 | i += 1 52 | if req != '_auto_' and kwargs['device'] is None: 53 | return 54 | self.stream = self.sd.InputStream(**kwargs) 55 | self.stream.start() 56 | self.samplerate = self.stream.samplerate 57 | 58 | def run(self): 59 | self.last_chunks = [] 60 | while self._thread_continue(): 61 | try: 62 | self._restart_if_needed() 63 | except Exception as e: 64 | self.error = str(e) 65 | time.sleep(.2) 66 | if self.stream and not self.stream.stopped: 67 | self.stream.stop() 68 | 69 | def _thread_continue(self): 70 | if self.kill_me: 71 | return False 72 | if not threading.main_thread().is_alive(): 73 | return False 74 | if not threading.current_thread().is_alive(): 75 | return False 76 | return True 77 | 78 | def _stream_cb(self, indata, frames, time, status): 79 | try: 80 | self.recorder.write(indata, int(self.samplerate), self.stream.channels) 81 | 82 | self.last_chunks.append(np.copy(indata)) 83 | if len(self.last_chunks) > 1000: 84 | self.last_chunks = self.last_chunks[-1000:] 85 | self.error = None 86 | if self.callback_data is None: 87 | self.callback_data = indata 88 | else: 89 | if len(indata[0]) != len(self.callback_data[0]): 90 | self.callback_data = indata 91 | else: 92 | self.callback_data = np.concatenate((self.callback_data[-1048576:], indata)) 93 | except Exception as e: 94 | print('audvis realtime recorder', e) 95 | -------------------------------------------------------------------------------- /analyzer/recording.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import os 3 | import random 4 | 5 | 6 | class AudVisRecorder: 7 | STATUS_RUNNING = 'running' 8 | STATUS_STOPPED = 'stop' 9 | status = STATUS_STOPPED 10 | 11 | data = None 12 | samplerate = None 13 | channels = None 14 | 15 | file = None # type: soundfile.SoundFile 16 | seconds = 0 17 | 18 | def start(self): 19 | self.status = 'running' 20 | 21 | def write(self, data, samplerate, channels): 22 | if self.status == self.STATUS_RUNNING: 23 | if self.data is None: 24 | self.data = data.copy() 25 | else: 26 | self.data = np.concatenate((self.data, data)) 27 | # self.seconds = len(self.file) / samplerate # TODO 28 | self.seconds = len(self.data) / samplerate # wrong, just testing 29 | self.samplerate = samplerate 30 | self.channels = channels 31 | 32 | def save_data(self, folder, format): 33 | import soundfile 34 | rnd = ''.join(random.choice("abcdefghijklmnopqrstuvwxyz") for i in range(6)) 35 | path = os.path.join(folder, "AudVisRecord-{}.{}".format(rnd, format.lower())) 36 | file = soundfile.SoundFile(path, mode='w', samplerate=self.samplerate, channels=self.channels, format=format) 37 | data = self.data 38 | self.data = None 39 | file.write(data) 40 | file.close() 41 | return path 42 | 43 | def stop(self): 44 | self.status = self.STATUS_STOPPED 45 | -------------------------------------------------------------------------------- /analyzer/sequence.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import os 3 | 4 | from .analyzer import Analyzer 5 | 6 | 7 | class _chunk: 8 | data = None 9 | time_from = -1 10 | time_to = -1 11 | 12 | 13 | class SequenceAnalyzer(Analyzer): 14 | audio = None 15 | sequence = None 16 | sequence_name = None 17 | whole_data = None 18 | chunk = None 19 | load_error = None 20 | 21 | def load(self, sequence=None): 22 | if self.load_error is not None: 23 | return 24 | self.whole_data = _chunk() 25 | self.chunk = _chunk() 26 | if sequence is None: 27 | return 28 | self.sequence_name = sequence.name 29 | # print('AudVis loading', sequence.name) 30 | self.sequence = sequence 31 | 32 | depsgraph = bpy.context.evaluated_depsgraph_get() 33 | if sequence.sound.packed_file is not None or os.path.exists(bpy.path.abspath(sequence.sound.filepath)): 34 | self.audio = sequence.sound.evaluated_get(depsgraph).factory 35 | self.samplerate = self.audio.specs[0] 36 | scene = bpy.context.scene 37 | self.on_pre_frame(scene, scene.frame_current_final) 38 | else: 39 | self.load_error = FileNotFoundError("sound file does not exist") 40 | return 41 | 42 | def on_pre_frame(self, scene, frame): 43 | if self.sequence not in scene.sequence_editor.sequences_all.values(): # pointer to sequence changes after any undo in Blender 44 | if self.sequence_name in scene.sequence_editor.sequences_all: 45 | self.sequence = scene.sequence_editor.sequences_all[self.sequence_name] # try to fix missing sequence 46 | else: 47 | self.empty() 48 | return 49 | self.sequence_name = self.sequence.name 50 | if frame < self.sequence.frame_final_start or frame > self.sequence.frame_final_end: 51 | self.empty() 52 | return 53 | if not self.audio: 54 | return 55 | 56 | fps = scene.render.fps / scene.render.fps_base 57 | 58 | time_to = scene.audvis.sequence_offset / 1000 + ( 59 | frame 60 | - self.sequence.frame_final_start 61 | + self.sequence.frame_offset_start 62 | + self.sequence.animation_offset_start 63 | ) / fps 64 | time_from = time_to - scene.audvis.sample_calc 65 | if scene.audvis.sequence_chunks: 66 | self._check_chunk(time_from, time_to) 67 | chunk = self.chunk 68 | else: 69 | if self.whole_data.data is None: 70 | self._load_whole_sound() 71 | chunk = self.whole_data 72 | soundframe_from = (time_from - chunk.time_from) * self.samplerate 73 | soundframe_from = max(0, soundframe_from) 74 | soundframe_to = (time_to - chunk.time_from) * self.samplerate 75 | soundframe_to = max(0, soundframe_to) 76 | row = chunk.data[int(soundframe_from):int(soundframe_to)] 77 | self.lastdata = row 78 | self.calculate_fft() 79 | 80 | def _load_whole_sound(self): 81 | self.whole_data.data = self.audio.data() 82 | self.whole_data.time_from = 0 83 | self.whole_data.time_to = self.audio.length / self.samplerate 84 | 85 | def _check_chunk(self, time_from, time_to): 86 | if self.chunk.data is not None \ 87 | and max(0, time_from) >= self.chunk.time_from \ 88 | and time_to <= self.chunk.time_to: 89 | return 90 | time_from = max(0, time_from) 91 | time_to = max(time_from, time_to + 10) 92 | # print("AAAAAAA", self.audio.length) 93 | self.chunk.data = self.audio.limit(time_from, time_to).data() 94 | self.chunk.time_from = time_from 95 | self.chunk.time_to = time_to 96 | # print("AudVis chunk", time_from, time_to) 97 | 98 | def stop(self): 99 | self.empty() 100 | self.audio = None 101 | self.sequence = None 102 | -------------------------------------------------------------------------------- /analyzer/shapemodifier/lib.py: -------------------------------------------------------------------------------- 1 | from random import Random 2 | 3 | from ...note_calculator import calculate_note 4 | import math 5 | 6 | 7 | def set_value(arr, index, value, operation): 8 | if operation == 'add': 9 | arr[index] += value 10 | elif operation == 'set': 11 | arr[index] = value 12 | 13 | 14 | def calc_driver_value(props, driver, index, weight=1): 15 | freq_from = 0 16 | freq_to = 0 17 | if props.freq_seq_type == 'notes': 18 | freq_from = calculate_note((index + props.note_offset) * props.note_step, props.note_a4_freq) 19 | freq_to = calculate_note((index + props.note_offset + 1) * props.note_step, props.note_a4_freq) 20 | elif props.freq_seq_type == 'classic': 21 | rng = props.freqrange 22 | freqstep = props.freq_step_calc 23 | freqstart = props.freqstart 24 | freq_from = index * freqstep + freqstart 25 | freq_to = index * freqstep + rng + freqstart 26 | 27 | if props.freq_seq_type == 'midi': 28 | driver_value = driver(midi=index + props.midi.offset, 29 | ch=props.midi.channel, 30 | track=props.midi.track, 31 | file=props.midi.file, 32 | device=props.midi.device) 33 | else: 34 | send_kwargs = {} 35 | if props.sound_sequence != '': 36 | send_kwargs['seq'] = props.sound_sequence 37 | if props.sequence_channel > 0: 38 | send_kwargs['seq_channel'] = props.sequence_channel 39 | if props.additive == 'off': 40 | driver_value = driver(freq_from, freq_to, ch=props.channel, **send_kwargs) 41 | else: 42 | tmp_val = driver(freq_from, freq_to, ch=props.channel, additive=True, **send_kwargs) 43 | if props.additive in ('sin', 'sin2'): 44 | tmp_val = math.sin(tmp_val * props.additive_phase_multiplier + props.additive_phase_offset) 45 | if props.additive == 'sin2': 46 | tmp_val = tmp_val / 2 + .5 # values range from 0 to 1 47 | elif props.additive == 'mod': 48 | tmp_val = (tmp_val * props.additive_phase_multiplier + props.additive_phase_offset) \ 49 | % props.additive_modulus 50 | elif props.additive == 'on': 51 | pass 52 | driver_value = tmp_val 53 | add = props.add 54 | if weight == 0 or props.factor == 0: 55 | return 0 56 | val = (driver_value + add / props.factor) * props.factor * weight 57 | # print(index, val, freq_from, freq_to) 58 | return val 59 | 60 | 61 | def sorted_freq_indexes(settings, count): 62 | freq_indexes = list(range(count)) 63 | order = settings.order 64 | if order == 'asc': 65 | pass 66 | elif order == 'desc': 67 | freq_indexes = freq_indexes[::-1] 68 | elif order == 'rand': 69 | seed = settings.random_seed 70 | Random(seed).shuffle(freq_indexes) 71 | return freq_indexes 72 | 73 | 74 | class location_setter: 75 | @staticmethod 76 | def vector(arr, index, vector, operation): 77 | set_value(arr, index * 3 + 0, vector[0], operation) 78 | set_value(arr, index * 3 + 1, vector[1], operation) 79 | set_value(arr, index * 3 + 2, vector[2], operation) 80 | 81 | @staticmethod 82 | def track_to(locations, i, vertex_co, val, this_obj, settings, operation): 83 | target_relative_location = this_obj.matrix_world.inverted() @ settings.track_object.location 84 | vector = (target_relative_location - vertex_co).normalized() * val 85 | set_value(locations, i * 3 + 0, vector[0], operation) 86 | set_value(locations, i * 3 + 1, vector[1], operation) 87 | set_value(locations, i * 3 + 2, vector[2], operation) 88 | -------------------------------------------------------------------------------- /analyzer/shapemodifier/uv.py: -------------------------------------------------------------------------------- 1 | from . import lib 2 | 3 | 4 | def _ensure_uv_layer(obj, name): 5 | if name in obj.data.uv_layers: 6 | return obj.data.uv_layers[name] 7 | return obj.data.uv_layers.new(name=name) 8 | 9 | 10 | def modify_uv(obj, scene, driver): 11 | settings = obj.audvis.shapemodifier 12 | from_name = "audvis from" 13 | to_name = "audvis to" 14 | uv_from = _ensure_uv_layer(obj, from_name) 15 | uv_to = _ensure_uv_layer(obj, to_name) 16 | data = [0] * (2 * len(uv_from.data)) 17 | pin_data = [False] * len(uv_from.data) 18 | uv_from.data.foreach_get('uv', data) 19 | uv_from.data.foreach_get('pin_uv', pin_data) 20 | usable_vertices_count = len(pin_data) - sum(pin_data) # sum of booleans... It's stupid, but it works. 21 | freq_indexes = lib.sorted_freq_indexes(obj.audvis.shapemodifier, usable_vertices_count) 22 | uv_vector = settings.uv_vector 23 | 24 | freq_i = 0 25 | for i in range(len(uv_from.data)): 26 | if pin_data[i]: 27 | continue 28 | value = lib.calc_driver_value(settings, driver, freq_indexes[freq_i], 1) 29 | lib.set_value(data, i * 2 + 0, value * uv_vector[0], obj.audvis.shapemodifier.operation) 30 | lib.set_value(data, i * 2 + 1, value * uv_vector[1], obj.audvis.shapemodifier.operation) 31 | freq_i += 1 32 | uv_to.data.foreach_set('uv', data) 33 | if obj.audvis.shapemodifier.is_baking: 34 | for uv_vertex in uv_to.data: 35 | uv_vertex.keyframe_insert("uv") 36 | -------------------------------------------------------------------------------- /analyzer/shapemodifier/vertcolor.py: -------------------------------------------------------------------------------- 1 | from . import lib 2 | 3 | 4 | def _ensure_color(obj, name): 5 | if name in obj.data.vertex_colors: 6 | return obj.data.vertex_colors[name] 7 | vc = obj.data.vertex_colors.new(name=name) 8 | arr = [0] * (4 * len(vc.data)) 9 | vc.data.foreach_set('color', arr) 10 | return vc 11 | 12 | 13 | def modify_color(obj, scene, driver): 14 | settings = obj.audvis.shapemodifier 15 | from_name = "audvis from" 16 | to_name = "audvis to" 17 | col_from = _ensure_color(obj, from_name) 18 | col_to = _ensure_color(obj, to_name) 19 | data = [0.0] * (4 * len(col_from.data)) 20 | col_from.data.foreach_get('color', data) 21 | usable_vertices_count = len(col_from.data) 22 | freq_indexes = lib.sorted_freq_indexes(obj.audvis.shapemodifier, usable_vertices_count) 23 | color = settings.vertcolor_color 24 | 25 | freq_i = 0 26 | for i in range(len(col_from.data)): 27 | if False: 28 | continue 29 | value = lib.calc_driver_value(settings, driver, freq_indexes[freq_i], 1) 30 | lib.set_value(data, i * 4 + 0, value * color[0], obj.audvis.shapemodifier.operation) 31 | lib.set_value(data, i * 4 + 1, value * color[1], obj.audvis.shapemodifier.operation) 32 | lib.set_value(data, i * 4 + 2, value * color[2], obj.audvis.shapemodifier.operation) 33 | freq_i += 1 34 | col_to.data.foreach_set('color', data) 35 | if obj.audvis.shapemodifier.is_baking: 36 | for col_vertex in col_to.data: 37 | col_vertex.keyframe_insert("color") 38 | -------------------------------------------------------------------------------- /analyzer/shapemodifier/vertexweight.py: -------------------------------------------------------------------------------- 1 | from random import Random 2 | 3 | from . import lib 4 | 5 | 6 | def ensure_vertex_group(obj, name): 7 | if name in obj.vertex_groups: 8 | return obj.vertex_groups[name] 9 | grp = obj.vertex_groups.new(name=name) 10 | grp.add(list(range(len(obj.data.vertices))), 0, 'REPLACE') 11 | return grp 12 | 13 | 14 | def modify_vertex_weight(obj, scene, driver): 15 | if obj.type != 'MESH' or obj.mode == 'EDIT': 16 | return 17 | grp_orig = ensure_vertex_group(obj, 'AudVis Origin') 18 | grp_target = ensure_vertex_group(obj, 'AudVis Target') 19 | lst = [] 20 | not_used_lst = [] 21 | for i in range(len(obj.data.vertices)): 22 | used = False 23 | for vertgroup in obj.data.vertices[i].groups: 24 | if vertgroup.group == grp_orig.index: 25 | lst.append(i) 26 | used = True 27 | if not used: 28 | not_used_lst.append(i) 29 | grp_target.remove(not_used_lst) 30 | order = obj.audvis.shapemodifier.order 31 | operation = obj.audvis.shapemodifier.operation 32 | if order == 'asc': 33 | pass 34 | elif order == 'desc': 35 | lst = lst[::-1] 36 | elif order == 'rand': 37 | seed = obj.audvis.shapemodifier.random_seed 38 | Random(seed).shuffle(lst) 39 | for i in range(len(lst)): 40 | val = lib.calc_driver_value(obj.audvis.shapemodifier, driver, i, 1) 41 | if operation == 'add': 42 | try: 43 | orig = grp_orig.weight(lst[i]) 44 | val += orig 45 | except Exception as e: 46 | val = 0 47 | grp_target.add([lst[i]], val, 'REPLACE') 48 | 49 | if obj.audvis.shapemodifier.is_baking: 50 | vertgroup_index = grp_target.index 51 | for vertex in obj.data.vertices: 52 | for group in vertex.groups: 53 | if group.group == vertgroup_index: 54 | group.keyframe_insert("weight") 55 | break 56 | -------------------------------------------------------------------------------- /analyzer/video/video_to_curve.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | NAME = "video to curve" 4 | LAYER_NAME = "video" 5 | 6 | 7 | def run(scene, conts): 8 | if scene.audvis.video_contour_object is None: 9 | curve = bpy.data.curves.new(name=NAME, type="CURVE") 10 | obj = bpy.data.objects.new(name=NAME, object_data=curve) 11 | scene.audvis.video_contour_object = obj 12 | bpy.context.collection.objects.link(obj) 13 | else: 14 | obj = scene.audvis.video_contour_object 15 | curve = obj.data 16 | 17 | for spline in curve.splines: 18 | curve.splines.remove(spline) 19 | for contour in conts: 20 | spline = curve.splines.new(type="NURBS") 21 | spline.points.add(len(contour)-1) 22 | for index in range(len(contour)): 23 | spline.points[index].co[:3] = contour[index] 24 | spline.points[index].co[3] = 1 25 | obj.update_tag() 26 | -------------------------------------------------------------------------------- /analyzer/video/video_to_gpencil.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | from ...utils import call_ops_override 4 | 5 | NAME = "video to gpencil" 6 | LAYER_NAME = "video" 7 | 8 | # TODO: 9 | """ 10 | bpy.ops.gpencil.trace_image({'active_object': bpy.data.objects['Empty'], 'selected_objects': [bpy.data.objects['GPencil']]}, target="SELECTED", thickness=10, use_current_frame=False) 11 | since 2.93 or 2.92 or 2.91 (warning: parameters were changed between versions) 12 | """ 13 | 14 | 15 | def run(scene, conts): 16 | if scene.audvis.video_contour_object is None: 17 | gpencil = bpy.data.grease_pencils.new(name=NAME) 18 | gpencil.pixel_factor = 20 19 | obj = bpy.data.objects.new(name=NAME, object_data=gpencil) 20 | call_ops_override(bpy.ops.object.material_slot_add, {'object': obj}) 21 | material = bpy.data.materials.new(name=NAME) 22 | bpy.data.materials.create_gpencil_data(material) 23 | obj.material_slots[0].material = material 24 | scene.audvis.video_contour_object = obj 25 | layer = gpencil.layers.new(name=LAYER_NAME) 26 | bpy.context.collection.objects.link(obj) 27 | else: 28 | obj = scene.audvis.video_contour_object 29 | gpencil = obj.data 30 | layer = gpencil.layers[LAYER_NAME] 31 | while len(layer.frames): 32 | layer.frames.remove(layer.frames[0]) 33 | frame = layer.frames.new(0) 34 | 35 | for contour in conts: 36 | stroke = frame.strokes.new() 37 | stroke.points.add(len(contour)) 38 | for index in range(len(contour)): 39 | stroke.points[index].co = contour[index] 40 | obj.update_tag() 41 | -------------------------------------------------------------------------------- /analyzer/video/video_to_mesh.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | NAME = "video to mesh" 4 | 5 | 6 | def run(scene, conts): 7 | if scene.audvis.video_contour_object is None: 8 | mesh = bpy.data.meshes.new(name=NAME) 9 | obj = bpy.data.objects.new(name=NAME, object_data=mesh) 10 | scene.audvis.video_contour_object = obj 11 | bpy.context.collection.objects.link(obj) 12 | else: 13 | obj = scene.audvis.video_contour_object 14 | mesh = obj.data 15 | if obj.mode != 'OBJECT': 16 | return 17 | 18 | mesh.clear_geometry() 19 | for contour in conts: 20 | old_verts_count = len(mesh.vertices) 21 | mesh.vertices.add(len(contour)) 22 | for index in range(len(contour)): 23 | mesh.vertices[old_verts_count + index].co = contour[index] 24 | old_edges_count = len(mesh.edges) 25 | if len(contour) >= 2: 26 | mesh.edges.add(len(contour) - 1) 27 | for index in range(1, len(contour) - 1): 28 | edge = mesh.edges[old_edges_count + index] 29 | edge.vertices = [old_verts_count + index - 1, old_verts_count + index] 30 | obj.update_tag() -------------------------------------------------------------------------------- /analyzer/video/video_to_object.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | from . import ( 4 | video_to_gpencil, 5 | video_to_mesh, 6 | video_to_curve, 7 | ) 8 | 9 | 10 | def run(scene, cv2): 11 | if not scene.audvis.video_contour_enable: 12 | return 13 | if scene.audvis.video_image is None: 14 | return 15 | vco = scene.audvis.video_contour_object 16 | if vco is not None and vco.type != scene.audvis.video_contour_object_type: 17 | scene.audvis.video_contour_object = None 18 | bpy.data.objects.remove(vco) 19 | vco = None 20 | if vco is not None and vco.name not in scene.objects and vco.users == 1: 21 | scene.audvis.video_contour_object = None 22 | bpy.data.objects.remove(vco) 23 | vco = None 24 | conts = _contours(cv2, scene) 25 | if scene.audvis.video_contour_object_type == 'GPENCIL': 26 | video_to_gpencil.run(scene, conts) 27 | elif scene.audvis.video_contour_object_type == 'MESH': 28 | video_to_mesh.run(scene, conts) 29 | elif scene.audvis.video_contour_object_type == 'CURVE': 30 | video_to_curve.run(scene, conts) 31 | 32 | 33 | def _contours(cv2, scene): 34 | props = scene.audvis 35 | img = cv2.imread(scene.audvis.video_image.filepath) 36 | if img is None: 37 | raise AssertionError("AudVis Contours: Image load error, because it's empty") 38 | imgGry = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) 39 | chain_approx = getattr(cv2, props.video_contour_chain_approx) 40 | ret, thrash = cv2.threshold(imgGry, props.video_contour_threshold, 255, chain_approx) 41 | contours, hierarchy = cv2.findContours(thrash, cv2.RETR_TREE, chain_approx) 42 | result = [] 43 | img_size = (len(img[0]), len(img)) 44 | size = max(img_size[0], img_size[1]) 45 | size = (1 / size) * props.video_contour_size 46 | move_by = ( 47 | img_size[0] * size / 2, 48 | img_size[1] * size / 2, 49 | ) 50 | for contour in contours: 51 | contour = cv2.approxPolyDP(contour, 52 | (props.video_contour_simplify / 1000) * cv2.arcLength(contour, True), 53 | True) 54 | if len(contour) >= props.video_contour_min_points_per_stroke: 55 | result.append([( 56 | c[0][0] * size - move_by[0], 57 | move_by[1] - c[0][1] * size, 58 | 0) for c in contour]) 59 | 60 | return result 61 | -------------------------------------------------------------------------------- /bge/__init__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | if 'bge' in sys.modules: 4 | from .bge_midi import MidiRealtime 5 | from .bge_realtime import Realtime 6 | from .bge_updater import Updater 7 | -------------------------------------------------------------------------------- /bge/bge_common.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import bpy 4 | 5 | import bge 6 | 7 | 8 | def update_callback(camera): 9 | # TODO: choose update method 10 | if False: # trigger bpy scene update 11 | bpy.context.scene.frame_set((bpy.context.scene.frame_current + 1) % 10000 + 1) 12 | else: # only call audvis updates 13 | bpy.audvis.update_data(bpy.context.scene, force=True) 14 | 15 | 16 | class CommonClass(bge.types.KX_PythonComponent): 17 | def start(self, args): 18 | if update_callback not in self.object.scene.pre_draw_setup: 19 | self.object.scene.pre_draw_setup.append(update_callback) 20 | self.driver = bpy.audvis.driver 21 | if hasattr(self, "_start"): 22 | self._start(args) 23 | 24 | def update(self): 25 | pass 26 | -------------------------------------------------------------------------------- /bge/bge_midi.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | 3 | from . import bge_common 4 | 5 | 6 | class MidiRealtime(bge_common.CommonClass): 7 | args = OrderedDict([ 8 | ("property_name", "prop"), 9 | ("note", 36), 10 | ("factor", 1.0), 11 | ("add", 0.0), 12 | ]) 13 | 14 | def _start(self, args): 15 | for key in args.keys(): 16 | setattr(self, key, args[key]) 17 | 18 | def update(self): 19 | val = self.driver( 20 | midi=self.note 21 | ) * self.factor + self.add 22 | self.object[self.property_name] = val 23 | # scene = bge.logic.getCurrentScene() 24 | # if val>2: 25 | # scene.addObject('Torus', self.object, 1.0) 26 | -------------------------------------------------------------------------------- /bge/bge_realtime.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | 3 | from . import bge_common 4 | 5 | 6 | class Realtime(bge_common.CommonClass): 7 | args = OrderedDict([ 8 | ("property_name", "prop"), 9 | ("freq_from", 50), 10 | ("freq_to", 200), 11 | ("factor", 1.0), 12 | ("add", 0.0), 13 | ]) 14 | 15 | def _start(self, args): 16 | for key in args.keys(): 17 | setattr(self, key, args[key]) 18 | 19 | def update(self): 20 | val = self.driver( 21 | self.freq_from, 22 | self.freq_to 23 | ) * self.factor + self.add 24 | self.object[self.property_name] = val 25 | # scene = bge.logic.getCurrentScene() 26 | # if val>2: 27 | # scene.addObject('Torus', self.object, 1.0) 28 | -------------------------------------------------------------------------------- /bge/bge_updater.py: -------------------------------------------------------------------------------- 1 | from . import bge_common 2 | 3 | 4 | class Updater(bge_common.CommonClass): 5 | pass 6 | -------------------------------------------------------------------------------- /blender_manifest.toml: -------------------------------------------------------------------------------- 1 | schema_version = "1.0.0" 2 | id = "audvis" 3 | version = "6.0.0" 4 | name = "AudVis" 5 | tagline = "Tools for awesome audio visualizations. Sound & Midi analyzers" 6 | maintainer = "example.sk " 7 | type = "add-on" 8 | website = "https://example.sk/audvis/" 9 | tags = ["Animation"] 10 | blender_version_min = "4.2.0" 11 | license = [ 12 | "SPDX:GPL-3.0-or-later", 13 | ] 14 | platforms = ["windows-x64", "macos-arm64", "macos-x64", "linux-x64"] 15 | wheels = [ 16 | "./wheels/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", 17 | "./wheels/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", 18 | "./wheels/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", 19 | "./wheels/cffi-1.17.1-cp311-cp311-win_amd64.whl", 20 | "./wheels/mido-1.3.2-py3-none-any.whl", 21 | "./wheels/opencv_python-4.10.0.84-cp37-abi3-macosx_11_0_arm64.whl", 22 | "./wheels/opencv_python-4.10.0.84-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", 23 | "./wheels/opencv_python-4.10.0.84-cp37-abi3-win_amd64.whl", 24 | "./wheels/packaging-23.2-py3-none-any.whl", 25 | "./wheels/pycparser-2.22-py3-none-any.whl", 26 | "./wheels/pygame-2.6.0-cp311-cp311-macosx_10_9_x86_64.whl", 27 | "./wheels/pygame-2.6.0-cp311-cp311-macosx_11_0_arm64.whl", 28 | "./wheels/pygame-2.6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", 29 | "./wheels/pygame-2.6.0-cp311-cp311-win_amd64.whl", 30 | "./wheels/sounddevice-0.5.0-py3-none-any.whl", 31 | "./wheels/sounddevice-0.5.0-py3-none-macosx_10_6_x86_64.macosx_10_6_universal2.whl", 32 | "./wheels/sounddevice-0.5.0-py3-none-win_amd64.whl", 33 | "./wheels/soundfile-0.12.1-py2.py3-none-macosx_10_9_x86_64.whl", 34 | "./wheels/soundfile-0.12.1-py2.py3-none-macosx_11_0_arm64.whl", 35 | "./wheels/soundfile-0.12.1-py2.py3-none-manylinux_2_31_x86_64.whl", 36 | "./wheels/soundfile-0.12.1-py2.py3-none-win_amd64.whl", 37 | ] 38 | 39 | [permissions] 40 | camera = "Put realtime camera into image" 41 | microphone = "Realtime music analyzing" -------------------------------------------------------------------------------- /build-extension.sh: -------------------------------------------------------------------------------- 1 | rm ./wheels/*.whl 2 | 3 | pip3.11 download -r requirements.txt --dest ./wheels --only-binary=:all: --python-version=3.11 4 | pip3.11 download -r requirements.txt --dest ./wheels --only-binary=:all: --python-version=3.11 --platform=macosx_11_0_arm64 5 | pip3.11 download -r requirements.txt --dest ./wheels --only-binary=:all: --python-version=3.11 --platform=macosx_12_0_x86_64 6 | pip3.11 download -r requirements.txt --dest ./wheels --only-binary=:all: --python-version=3.11 --platform=win_amd64 7 | 8 | pip3.11 download -r requirements-nodeps.txt --no-deps --dest ./wheels --only-binary=:all: --python-version=3.11 9 | pip3.11 download -r requirements-nodeps.txt --no-deps --dest ./wheels --only-binary=:all: --python-version=3.11 --platform=macosx_11_0_arm64 10 | pip3.11 download -r requirements-nodeps.txt --no-deps --dest ./wheels --only-binary=:all: --python-version=3.11 --platform=macosx_12_0_x86_64 11 | pip3.11 download -r requirements-nodeps.txt --no-deps --dest ./wheels --only-binary=:all: --python-version=3.11 --platform=win_amd64 12 | 13 | echo "LIST OF WHEELS:\n" 14 | find ./wheels | sort 15 | 16 | /opt/blender/4.2/blender --command extension build --split-platforms --output-dir=.. 17 | 18 | /opt/blender/4.3/blender --command extension install-file -r user_default ../audvis-6.0.0-linux_x64.zip 19 | /opt/blender/4.3/blender 20 | -------------------------------------------------------------------------------- /doc/armature.md: -------------------------------------------------------------------------------- 1 | # Armature Generator 2 | 3 | This feature gets **selected** faces of your **mesh** object, and creates a new armature object + a new Armature 4 | modifier on the original mesh object. All the created bones will be animated along the face's normal, thus move the face 5 | along its normal. This is done by creating a driver for the location Y for every single created bone. With default 6 | settings, one of the bones will have a driver expression like this `audvis(450.0, 500.0) * 0.1` 7 | 8 | Video: [![IMAGE ALT TEXT HERE](https://img.youtube.com/vi/e0v9dO3wU1s/0.jpg)](https://www.youtube.com/watch?v=e0v9dO3wU1s) 9 | 10 | ## Settings: 11 | 12 | - **Inset** - performs also "Inset Faces" on all the selected faces. Only the new created faces will be animated. 13 | - **Extrude After Inset** - after "Inset Faces", also "Extrude" will be performed. Only the new faces after 14 | extruding will be animated 15 | - **Inset Size** - this is the same like "Thickness" property of the "Inset Faces" operator. 16 | - **Multiple Freq properties** - the whole concept is [described here](./freq-sequencing.md) 17 | - **Keep Old Vertex Groups** 18 | - if disabled, for every clicking on Generate AudVis Armature, AudVis will delete all the vertex groups with names 19 | starting with AudvisArm 20 | - if enabled, only new vertex groups will be created, so you can use the same armature, but add new animated bones. 21 | In this case, don't forget to set **Frequency Start** 22 | - **Generate AudVis Armature** - perform the generating process. Please, always save your project before doing this, so 23 | you can easily return to previous state in case you don't like the result. Mainly if using Inset and Extrude. -------------------------------------------------------------------------------- /doc/bake-drivers.md: -------------------------------------------------------------------------------- 1 | # Bake Drivers 2 | 3 | Finds all the drivers using audvis() expressions everywhere and bakes them into classical animation data, so you can 4 | render your animation on a render farm or on a PC without AudVis installed. 5 | 6 | ## Settings 7 | 8 | - **Bake Drivers** - start baking all the drivers. You can stop it by pressing Esc 9 | - **Nonstop Baking Collection** - if a collection is selected, baking will be performed on the go, as YOU move along the 10 | frames. If you jump to frame 143, data will be baked to the frame 143. This can be changed in the future. 11 | - **Edit Driver Expressions Here** only if **Nonstop Baking Collection** is selected. If enabled, you will see a list of all driver expressions in the selected collection 12 | -------------------------------------------------------------------------------- /doc/driver-values.md: -------------------------------------------------------------------------------- 1 | # Driver Values panel 2 | 3 | [Realtime](./realtime.md) and [Sequence](./sequence.md) analyzers produce data. In Driver Values you can tweak them. 4 | 5 | ## Settings: 6 | 7 | - if **Use Better Filters** is disabled (default): 8 | - **Highpass Frequency** and **Highpass Strength** - a 9 | - if **Use Better Filters** is enabled: 10 | - before FFT is performed, current sound chunk data is loaded into `aud.Sound` object and then these things are 11 | applied: 12 | - **Highpass Frequency** and **Q** - [highpass](https://docs.blender.org/api/current/aud.html#aud.Sound.highpass) 13 | - **Lowpass Frequency** and **Q** - [lowpass](https://docs.blender.org/api/current/aud.html#aud.Sound.lowpass) 14 | - **Attack, Decay, Sustain, Release** - [ADSR](https://docs.blender.org/api/current/aud.html#aud.Sound.ADSR) 15 | - **Use Curve** - if enabled, you can make your own highpass and lowpass filter - left part of the curve is for 16 | lower frequencies, right part of the curve is for highest frequencies 17 | - **Factor** - all values will be multiplied by this value 18 | - **Max** - doesn't allow values higher than this 19 | - **Add** - adds this number to all calculated values 20 | - **Add Noise** - adds 21 | - **Normalize Values** - just try it, I don't know 22 | - **Fade Out Type**: 23 | - **Off** - don't fade out 24 | - **Linear** - returns higher value of "previous frame value - **Fade Out Speed**" or "current value" 25 | - **Exponential** - returns higher value of "previous frame value * (1 - **Fade Out Speed**)" or "current value" 26 | - **Natural** - similar to exponential, but much more natural results. The formula is 27 | - `previous_fft * (1 - fadeout_speed) + fft * fadeout_speed` 28 | - warning: if you jump between frames, the results can be inconsistent -------------------------------------------------------------------------------- /doc/drivers.md: -------------------------------------------------------------------------------- 1 | # Drivers 2 | 3 | You can animate many, many things by writing your own drivers. Just right click on any numeric value, for example 4 | object's Location X, and click "Add Driver". Then write any expression. 5 | 6 | Simplest formula looks like this: `audvis(100, 200)` - this will animate your property by sound frequencies 100Hz - 7 | 200Hz. 8 | 9 | ## Python driver expressions 10 | 11 | You want to read the manual [here](https://docs.blender.org/manual/en/latest/animation/drivers/drivers_panel.html), 12 | [here](https://docs.blender.org/manual/en/latest/animation/drivers/introduction.html) 13 | and [especially here](https://docs.blender.org/manual/en/latest/animation/drivers/drivers_panel.html#drivers-simple-expressions) 14 | . 15 | 16 | Here are some simple expressions to show what you probably want to do: 17 | 18 | - `audvis(100, 200) * 10 + 100` 19 | - `3 - audvis(100, 200) / 15` 20 | - `clamp(audvis(100, 200) / 3, 0, 3)` 21 | 22 | ## Expressions for Realtime and Sequence analyzers: 23 | 24 | - **seq** - `audvis(100, 200, seq="MyGreatSong.mp3")` - only react to MyGreatSong.mp3 25 | - **ch** - `audvis(100, 200, ch=2)` - channel. By default, only one channel is parsed, so if you want to use more of 26 | them, you need to increase the value of `Channels Count` property in the main AudVis panel 27 | - **seq** and **ch** can be combined - `audvis(100, 200, seq="MyGreatSong.mp3", ch=2)` 28 | 29 | ## Expressions for MIDI File and MIDI Realtime analyzers: 30 | 31 | - **midi** - `audvis(midi=1)` - note 1. This parameters is needed if you want to use any midi, or 32 | - **midi** - `audvis(midi=[40, 50])` - max. value from notes 40-50 33 | - **midi** - `audvis(midi=[40, 50, 'avg'])` - average value from notes 40-50 34 | - **midi** - `audvis(midi=[40, 50, 'avg_nonzero'])` - average of non-zero values from notes 40-50 35 | - **midi** - `audvis(midi=[40, 50, 'sum'])` - sum of values from notes 40-50 36 | - **device** - `audvis(midi=1, device="MIDI Device 1")` - name of the device from the MIDI Realtime panel 37 | - **ch** - `audvis(midi=1, ch="1")` - midi channel 38 | - **file** - `audvis(midi=3, file='MyGreatSong.mid')` - file name from the MIDI File panel 39 | - **track** - `audvis(midi=3, file='MyGreatSong.mid', track='Track 1')` - track from the file. If your tracks in the 40 | MIDI are not named, they will get names like "Track 1" 41 | - you can combine **device** + **ch**, or **file** + **track** + **ch**, but **device** + **file** makes absolutely no 42 | sense. 43 | 44 | ## Some ideas what to animate with drivers: 45 | 46 | - object's basic properties like location, rotation, scale 47 | - shape key value 48 | - array modifier's Count property 49 | - `Object -> Viewport Display -> Color` and use it inside shading nodes` 50 | - armature bones' properties - rotation and/or scale or event Bone Constraints 51 | - light color / power / radius... 52 | - camera focal length 53 | - almost any nodes in geometry nodes, shading, compositing... 54 | - depth of field settings 55 | - just experiment! Explore all the possibilities (impossible) and have fun -------------------------------------------------------------------------------- /doc/example-objects.md: -------------------------------------------------------------------------------- 1 | ## TODO -------------------------------------------------------------------------------- /doc/freq-sequencing.md: -------------------------------------------------------------------------------- 1 | # Concept: Frequency Sequencing 2 | 3 | [Shape Modifier](./shape-modifier.md), [Spectrogram](./spectrogram.md), [Generate Example Objects](./example-objects.md) 4 | , [Generate Armature](./armature.md) use the same concept. AudVis iterates over a list of points/bones/objects and moves 5 | them somehow. Here is explanation. 6 | 7 | For simplicity, word "point" is used here, but it means vertex, face, object, bone or whatever is correct in the 8 | context. 9 | 10 | ## Settings 11 | 12 | - **Freq Sequencing** 13 | - **Linear** 14 | - **Frequency Range Per Point** - if set to 50, every item will be animated by range of 50Hz. For example first 15 | point will be animated by 0-50Hz, second one (according to other settings) 50-100Hz or 10-60Hz... and so on 16 | - **Frequency Start** - how many Hz to skip. If set to 200, first point will be animated for example by 17 | 200-250Hz 18 | - **Set Custom Step** and **Frequency Step** 19 | - if disabled, the sequence will be 0-50Hz, 50-100Hz, 100-150Hz... 20 | - if enabled and the Step set to 10, the sequence will be: 0-50Hz, 10-60Hz, 20-70Hz... 21 | - **Notes** - [wiki](https://en.wikipedia.org/wiki/Piano_key_frequencies) 22 | - **A4 Note Frequency** - default 440Hz, but you can set it to whatever you want 23 | - **Note Step** - if you have a lot of points to animate, you probably want to lower this number. If not, keep 24 | it on 1.0 25 | - **Note Step Offset** - on what note you want to start - affected by **Note Step**. If you have a lot of bass, 26 | you probably want to use negative values for this property 27 | - **MIDI Notes** - every point is 1 midi note 28 | - **MIDI Note Offset** - if you are interested only in notes higher than 40, set this property to 40 29 | - There is also one line telling you what sequences you will get in your animation. For example 0.00 - 4,150.00 Hz. 30 | If you see too high numbers here, for example over 20 000 Hz, you want to change your settings to get lower 31 | number. If your sound input's sampling frequency is 44100 Hz, FFT used by AudVis can't give you numbers over 32 | 22050Hz (sampling frequency / 2). -------------------------------------------------------------------------------- /doc/img/basic-view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/example-sk/audvis/35e7aa673bd9d57cfe9175937fca273fb4c194f7/doc/img/basic-view.png -------------------------------------------------------------------------------- /doc/img/curve-anim.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/example-sk/audvis/35e7aa673bd9d57cfe9175937fca273fb4c194f7/doc/img/curve-anim.gif -------------------------------------------------------------------------------- /doc/img/generator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/example-sk/audvis/35e7aa673bd9d57cfe9175937fca273fb4c194f7/doc/img/generator.png -------------------------------------------------------------------------------- /doc/img/sequence-analyzer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/example-sk/audvis/35e7aa673bd9d57cfe9175937fca273fb4c194f7/doc/img/sequence-analyzer.png -------------------------------------------------------------------------------- /doc/img/shape-modifier.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/example-sk/audvis/35e7aa673bd9d57cfe9175937fca273fb4c194f7/doc/img/shape-modifier.png -------------------------------------------------------------------------------- /doc/midi-file.md: -------------------------------------------------------------------------------- 1 | # MIDI File 2 | 3 | If you want to animate things by MIDI File, this is what you are searching for. AudVis will read the file using Python 4 | module mido, and save the parsed data into animation data. 5 | 6 | ## Usage in driver expressions: 7 | 8 | - `audvis(midi=30)` 9 | - `audvis(midi=30, ch=3)` 10 | - `audvis(midi=30, device="My cool midi device name")` 11 | - `audvis(midi=30, track="Drone track")` 12 | - `audvis(midi=30, file="MyBestSongEver.mid")` 13 | 14 | ## Settings 15 | 16 | - **Enable** (in the Midi File panel header): do you want to use Midi File feature? If not, keep this disabled 17 | - **Enable** single midi file - you can enable/disable every single midi file for use in AudVis 18 | - **Add Midi File** - choose a midi file to use. You can use multiple midi files, and animate different things by 19 | different midi files 20 | - **Remove Midi File** - remove selected midi file 21 | - **Name** - internal name of the midi file. You can change it or copy the name to use it in the [driver](./drivers.md) 22 | expression 23 | - **Frame Start** - frame when to start using this midi file 24 | - **Hold Offset Start** - just offset - you can cut out the start of the song for example 25 | - **Hold Offset End** - just offset - you can cut out the end of the song for example 26 | - **Tracks** - list of tracks inside selected file 27 | - **Track Name** - You can change it or copy the name to use it in the [driver](./drivers.md) expression 28 | - **Remove Midi Track** - you can remove single track from a midi file (this doesn't affect the MIDI file on the disk) -------------------------------------------------------------------------------- /doc/midi-realtime.md: -------------------------------------------------------------------------------- 1 | # MIDI Realtime 2 | 3 | ## Usage in driver expressions: 4 | 5 | You can combine parameters, but combining midi=30 (note) and midi_control=83 (control) doesn't make sense 6 | - `audvis(midi=30)` - midi note 30 7 | - `audvis(midi=30, ch=3)` - channel 3 8 | - `audvis(midi=30, device="My cool midi device")` - only midi notes from this device 9 | - `audvis(midi_control=83)` 10 | 11 | ## Settings 12 | 13 | - **Enable** (in the Midi Realtime panel header): do you want to use Midi Realtime feature? If not, keep this disabled 14 | - checkbox inside the list of inputs: enable / disable single device 15 | - **Add Midi Input**: adds midi input 16 | - **Name**: Name to refer in driver expressions 17 | - **Input Device**: select the device connected to your computer 18 | - **Restart Midi Inputs**: restarts the backend. Use when in trouble 19 | - **Debug Realtime Midi Messages**: write last midi note / midi control message in the workspace status text (left 20 | bottom corner) -------------------------------------------------------------------------------- /doc/packages-install.md: -------------------------------------------------------------------------------- 1 | # Installing Python Packages 2 | 3 | If you want to use [Realtime Analyzer](./realtime.md), 4 | [MIDI File](./midi-file.md), [MIDI Realtime](./midi-realtime.md) 5 | or [Video Capture](./video-capture.md), you need to install python 6 | packages. Everything should be done only by clicking on button 7 | "Install python packages" under any of mentioned panels. After clicking 8 | on this button, a new window opens with installation log. If everything 9 | works fine, you should see something like this: 10 | 11 | ``` 12 | """ 13 | RUN /opt/blender/3.1/3.1/python/bin/python3.9 -m ensurepip --altinstall --user 14 | 15 | info 2.8s: Looking in links: /tmp/tmpikxtby4l 16 | info 2.8s: Requirement already satisfied: setuptools in ./.local/lib/python3.9/site-packages (49.2.1) 17 | info 2.8s: Processing /tmp/tmpikxtby4l/pip-21.2.3-py3-none-any.whl 18 | info 2.9s: Installing collected packages: pip 19 | info 4.3s: Successfully installed pip-21.2.3 20 | 21 | ... (more lines) ... 22 | 23 | RUN /opt/blender/3.1/3.1/python/bin/python3.9 -m pip install --force-reinstall --upgrade --target /home/r/.config/blender/3.1/scripts/addons/modules/_audvis_modules/a_1639510547 --no-deps opencv-python 24 | 25 | info 1.8s: Collecting opencv-python 26 | info 1.9s: Downloading opencv_python-4.5.4.60-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (60.3 MB) 27 | info 8.4s: Installing collected packages: opencv-python 28 | info 9.9s: Successfully installed opencv-python-4.5.4.60 29 | 30 | 31 | FINISHED! 32 | 33 | 34 | """ 35 | ``` 36 | 37 | ## Troubleshooting: 38 | 39 | If you see errors in the log, you can go to `Edit -> Preferences -> Add-ons -> AudVis` 40 | and change the "Install directory" to "Blender" instead of default 41 | "User Add-On Modules", and click on (Re)Install button. 42 | 43 | ### Windows: 44 | - You may need to install [Microsoft Visual C++ 2015 Redistributable Update 3 RC.](https://www.microsoft.com/en-us/download/details.aspx?id=52685) 45 | - Try running Blender as Administrator, and press install button again. ([how to run a program as administrator](https://www.youtube.com/watch?v=nNVdaJXYCbA)) 46 | - If you want to use Realtime Analyzer, setup Stereo Mix ([video instructions here](https://youtu.be/Bd3moKLV5sE)) 47 | 48 | ### Linux: 49 | - You may need to install libportaudio2 (on debian or ubuntu, the command is `sudo apt-get install libportaudio2`). 50 | 51 | ### MacOs: 52 | - Mac computers come with different processor types. Only tested with MacBook Air mid 2012. It would be nice if you test this on other hardware and let me know if it worked. 53 | - For Realtime Analyzer, you may need to install Soundflower or similar software ([video instructions here](https://www.youtube.com/results?search_query=soundflower+macos). 54 | - Warning: it's possible you will need to run Blender from terminal to use realtime audio or video capture features. MacOs has a strict security feature that doesn't allow an app to use, for example, Camera, unless the app asks for a permission. Blender itself doesn't ask for it, so you need to run Blender from terminal, so Terminal can ask for permission. 55 | -------------------------------------------------------------------------------- /doc/realtime.md: -------------------------------------------------------------------------------- 1 | # Realtime Analyzer 2 | 3 | Read first: [Installing Python Packages](./packages-install.md) 4 | 5 | Realtime Analyzer is the first part of AudVis and the reason, why this whole project has started. 6 | 7 | AudVis reads data from your microphone or sound capture using Python package `sounddevice`. Most of the setup is usually 8 | done outside of Blender and AudVis. You need to setup your operating system settings: 9 | - on Linux, everything should work as expected 10 | - on Windows, setup [Stereo Mix](https://www.google.com/search?q=windows+stereo+mix) 11 | - on MacOS, you need a 3rd party software like [Soundflower](https://www.google.com/search?q=soundflower) or [Blackhole](https://www.google.com/search?q=macos+blackhole) 12 | 13 | Sometimes it can frustrating and sometimes, in case of some exotic hardware combinations, it can be almost impossible to make 14 | everything work. You have been warned. 15 | 16 | If everything works, [Fast Fourier Transform](https://en.wikipedia.org/wiki/Fast_Fourier_transform) is performed for 17 | every frame of Blender scene. This means you need to play animation to watch any animation. The result of FFT is stored 18 | and then used when needed, for example by [drivers](./drivers.md), [Shape modifier](./shape-modifier.md) 19 | or [Spectrogram](./spectrogram.md). 20 | 21 | ## Settings: 22 | 23 | - **Enable** (in the Realtime Analyzer panel header): do you want to use Real Time Analyzer? If not, keep this disabled 24 | - **Input Device**: choose the device you want to "listen" 25 | - **Auto Switch Scenes**: if you have multiple scenes, they will alternate. Please, don't use scenes with too small 26 | number of frames -------------------------------------------------------------------------------- /doc/scripting.md: -------------------------------------------------------------------------------- 1 | ## TODO -------------------------------------------------------------------------------- /doc/scripts/curve.py: -------------------------------------------------------------------------------- 1 | """ 2 | press ALT+P to run this script 3 | """ 4 | 5 | import math 6 | 7 | import bpy 8 | 9 | POINTS_COUNT = 30 10 | WIDTH = 5 11 | 12 | xxx = WIDTH / POINTS_COUNT 13 | 14 | curve = None 15 | if "BezierCurve" in bpy.context.scene.objects: 16 | curve = bpy.context.scene.objects["BezierCurve"].data 17 | else: 18 | bpy.ops.curve.primitive_bezier_curve_add() 19 | curve = bpy.context.object.data 20 | bpy.context.object.rotation_euler[0] = math.pi / 2 21 | curve.dimensions = '2D' 22 | curve.bevel_depth = .1 23 | 24 | curve.splines.remove(curve.splines[0]) 25 | 26 | spline = curve.splines.new('BEZIER') 27 | spline.use_cyclic_u = True 28 | spline.bezier_points.add(POINTS_COUNT + 1) 29 | 30 | 31 | def callback(driver): 32 | spline = curve.splines[0] 33 | loc = (0, 0, 0) 34 | 35 | point = spline.bezier_points[0] 36 | point.co = (0, 0, 0) 37 | point.handle_left = point.co 38 | point.handle_right = point.co 39 | 40 | point = spline.bezier_points[-1] 41 | point.co = (WIDTH + xxx, 0, 0) 42 | point.handle_right = point.co 43 | point.handle_left = point.co 44 | 45 | for i in range(1, len(spline.bezier_points) - 1): 46 | val = driver(i * 30, (i + 1) * 30) * .1 47 | point = spline.bezier_points[i] 48 | loc = (i / POINTS_COUNT * WIDTH, val, 0) 49 | point.co = loc 50 | point.handle_right = (loc[0] + xxx, loc[1], loc[2]) 51 | point.handle_left = (loc[0] - xxx, loc[1], loc[2]) 52 | 53 | 54 | bpy.audvis.register_script("test", callback) 55 | -------------------------------------------------------------------------------- /doc/scripts/curve2.py: -------------------------------------------------------------------------------- 1 | """ 2 | press ALT+P to run this script 3 | """ 4 | 5 | import bpy 6 | 7 | curve = bpy.context.scene.objects["BezierCurve"].data 8 | 9 | 10 | def clean(): 11 | curve.splines.remove(curve.splines[0]) 12 | curve.splines.new('BEZIER') 13 | 14 | 15 | clean() 16 | 17 | 18 | def callback(driver): 19 | if bpy.context.scene.frame_current == 1: 20 | clean() 21 | spline = curve.splines[0] 22 | spline.bezier_points.add(1) 23 | point = spline.bezier_points[-1] 24 | point.co = (len(spline.bezier_points) / 2, 0, 0) 25 | point.radius = driver(10, 100) 26 | point.handle_left = point.co 27 | point.handle_right = point.co 28 | 29 | 30 | bpy.audvis.register_script("test", callback) 31 | -------------------------------------------------------------------------------- /doc/scripts/curve3.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import random 3 | 4 | freq_range = 50 5 | curve = bpy.data.curves['BezierCircle'] 6 | 7 | 8 | def callback(driver): 9 | i = 0 10 | for spline in curve.splines: 11 | points = spline.bezier_points 12 | for point in points: 13 | i += 1 14 | frm = i * freq_range 15 | to = frm + freq_range 16 | val = driver(frm, to) / 30 + .02 17 | val += random.random() * .1 18 | point.radius = val 19 | 20 | 21 | bpy.audvis.register_script("test", callback) 22 | -------------------------------------------------------------------------------- /doc/scripts/empty.py: -------------------------------------------------------------------------------- 1 | """ 2 | press ALT+P to run this script 3 | """ 4 | 5 | import math 6 | import bpy 7 | 8 | def callback(driver): 9 | print(driver(10, 100)) 10 | 11 | 12 | bpy.audvis.register_script("test", callback) 13 | -------------------------------------------------------------------------------- /doc/scripts/gpencil-ekg.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | for g in bpy.data.grease_pencils: 4 | bpy.data.grease_pencils.remove(g) 5 | 6 | bpy.ops.object.gpencil_add(radius=1, view_align=False, location=(0, 0, 0), rotation=(0, 0, 0), type='EMPTY') 7 | gpencil = bpy.context.object.data 8 | gpencil.layers.new("") 9 | layer = gpencil.layers.new("") 10 | frame = layer.frames.new(1) 11 | strokes = [frame.strokes.new() for i in range(15)] 12 | 13 | r = 50 14 | max_values = 70 15 | 16 | i = [-1] 17 | 18 | 19 | def callback(driver): 20 | i[0] += 1 21 | v = i[0] % max_values 22 | for j in range(len(strokes)): 23 | stroke = strokes[j] 24 | val = driver(j * r, (j + 1) * r) * 3 25 | if len(stroke.points) < max_values: 26 | stroke.points.add(1, pressure=.1, strength=1) 27 | p = stroke.points[v] 28 | p.co[0] = v / 10 - 3.2 29 | p.co[2] = j / 4 + val / 100 - 1.7 30 | 31 | 32 | bpy.audvis.register_script("test", callback) 33 | -------------------------------------------------------------------------------- /doc/scripts/gpencil-ekg2.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | for g in bpy.data.grease_pencils: 4 | bpy.data.grease_pencils.remove(g) 5 | 6 | bpy.ops.object.gpencil_add(radius=1, view_align=False, location=(0, 0, 0), rotation=(0, 0, 0), type='EMPTY') 7 | obj = bpy.context.object 8 | obj.grease_pencil_modifiers.new("", type="GP_THICK").thickness = 50 9 | gpencil = obj.data 10 | gpencil.layers.new("") 11 | layer = gpencil.layers.new("") 12 | frame = layer.frames.new(1) 13 | strokes = [frame.strokes.new() for i in range(15)] 14 | 15 | r = +50 16 | max_values = 70 * 3 17 | 18 | j = -1 19 | for stroke in strokes: 20 | j += 1 21 | for a in range(max_values): 22 | stroke.points.add(1, pressure=1, strength=1) 23 | p = stroke.points[-1] 24 | p.co[0] = a / 30 - 3.2 25 | p.co[2] = j / 4 - 1.7 26 | 27 | i = [-1] 28 | 29 | 30 | def callback(driver): 31 | i[0] += 1 32 | v = i[0] % max_values 33 | for j in range(len(strokes)): 34 | stroke = strokes[j] 35 | val = driver(j * r, (j + 1) * r) * 3 36 | p = stroke.points[v] 37 | p.co[2] = j / 4 + val / 100 - 1.7 38 | 39 | 40 | bpy.audvis.register_script("test", callback) 41 | -------------------------------------------------------------------------------- /doc/scripts/gpencil-from-camera.py: -------------------------------------------------------------------------------- 1 | """ 2 | press ALT+P to run this script 3 | """ 4 | 5 | import bpy 6 | import cv2 7 | 8 | OBJECT_NAME = "video to gpencil" 9 | if OBJECT_NAME in bpy.data.objects: 10 | bpy.data.objects.remove(bpy.data.objects[OBJECT_NAME]) 11 | bpy.ops.object.add(type='GPENCIL') 12 | obj = bpy.context.object 13 | obj.name = OBJECT_NAME 14 | layer = obj.data.layers.new(name="") 15 | 16 | 17 | def callback(driver): 18 | while len(layer.frames): 19 | layer.frames.remove(layer.frames[0]) 20 | frame = layer.frames.new(1) 21 | img = cv2.imread(bpy.context.scene.audvis.video_image.filepath) 22 | imgGry = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) 23 | chain_approx = cv2.CHAIN_APPROX_NONE 24 | chain_approx = cv2.CHAIN_APPROX_SIMPLE 25 | chain_approx = cv2.CHAIN_APPROX_TC89_KCOS 26 | # chain_approx = cv2.CHAIN_APPROX_TC89_L1 27 | ret, thrash = cv2.threshold(imgGry, 100 , 150, chain_approx) 28 | contours , hierarchy = cv2.findContours(thrash, cv2.RETR_TREE, chain_approx) 29 | bbb = 0 30 | d = 100 - driver(10, 500)*500 31 | d = max(10, d) 32 | d = 15 33 | for contour in contours: 34 | contour = cv2.approxPolyDP(contour, 0.002 * cv2.arcLength(contour, True), True) 35 | if len(contour) < d: 36 | continue 37 | stroke = frame.strokes.new() 38 | bbb += 1 39 | stroke.line_width = driver(bbb*100, bbb*100+100) * 150 + .01 40 | stroke.points.add(len(contour)) 41 | size = max(len(img), len(img[0])) 42 | size = 1/size 43 | for aaa in range(len(contour)): 44 | bbb += 1 45 | stroke.points[aaa].co = ( 46 | -contour[aaa][0][0] * size + .5, 47 | -contour[aaa][0][1] * size + .5, 48 | 0 49 | ) 50 | 51 | callback(bpy.audvis.driver) 52 | 53 | 54 | bpy.audvis.register_script("test", callback) 55 | -------------------------------------------------------------------------------- /doc/scripts/gpencil-hair.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | xcount = 10 4 | ycount = 100 5 | 6 | layer = bpy.data.grease_pencils[0].layers[0] 7 | def cb(driver): 8 | while len(layer.frames): 9 | layer.frames.remove(layer.frames[0]) 10 | frame = layer.frames.new(1) 11 | for j in range(ycount): 12 | stroke = frame.strokes.new() 13 | stroke.points.add(count=xcount, pressure=1, strength=1) 14 | for i in range(xcount): 15 | point = stroke.points[i] 16 | val = driver(i + j * xcount, i + 1 + j * xcount) 17 | point.pressure = val 18 | point.co = (i * .3, j * .1, val) 19 | 20 | bpy.audvis.register_script('test', cb) -------------------------------------------------------------------------------- /doc/scripts/gpencil-spiral.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import math 3 | 4 | for g in bpy.data.grease_pencils: 5 | bpy.data.grease_pencils.remove(g) 6 | 7 | # CONFIG 8 | move_z = .0008 9 | rot_z = .03 10 | count = 1 11 | init_length = 1000 12 | max_length = 2000 13 | 14 | # INIT 15 | objects = [] 16 | i = 0 17 | 18 | for j in range(count): 19 | bpy.ops.object.gpencil_add(radius=1, location=(j - count/2+.5, 0, 0), rotation=(0, 0, 0), type='EMPTY') 20 | objects.append(bpy.context.object) 21 | gpencil = bpy.context.object.data 22 | layer = gpencil.layers.new("") 23 | frame = layer.frames.new(1) 24 | frame.strokes.new() 25 | 26 | # MAIN CODE 27 | def stroke_cb(stroke_index, driver): 28 | global i 29 | 30 | obj = objects[stroke_index] 31 | stroke = obj.data.layers[0].frames[0].strokes[0] 32 | hz = stroke_index * 100 33 | 34 | stroke.points.add(1, pressure=.1, strength=1) 35 | p = stroke.points[-1] 36 | p.co[0] = math.cos(i * rot_z) * .4 37 | p.co[1] = math.sin(i * rot_z) * .4 38 | val = 0 39 | if driver: 40 | val = driver(hz, 100 + hz) + 1 41 | p.pressure = val + 1 42 | p.co[2] = val / 100 - obj.location[2] 43 | 44 | if len(stroke.points) > max_length: 45 | stroke.points.pop(index=0) 46 | 47 | def callback(driver): 48 | global i 49 | i += 1 50 | 51 | for j in range(len(objects)): 52 | obj = objects[j] 53 | obj.location[2] = -i * move_z 54 | obj.rotation_euler[2] = -i * rot_z + math.pi*1.5 55 | stroke_cb(j, driver) 56 | 57 | 58 | for j in range(init_length): 59 | callback(None) 60 | 61 | bpy.audvis.register_script("test", callback) 62 | -------------------------------------------------------------------------------- /doc/scripts/gpencil-worms.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import math 3 | import random 4 | 5 | for g in bpy.data.grease_pencils: 6 | bpy.data.grease_pencils.remove(g) 7 | 8 | bpy.ops.object.gpencil_add(radius=1, location=(0, 0, 0), rotation=(0, 0, 0), type='EMPTY') 9 | gpencil = bpy.context.object.data 10 | gpencil.layers.new("") 11 | layer = gpencil.layers.new("") 12 | frame = layer.frames.new(1) 13 | strokes = [frame.strokes.new() for i in range(15)] 14 | [stroke.points.add(1) for stroke in strokes] 15 | r = 10 16 | max_values = 150 17 | 18 | 19 | def callback(driver): 20 | for j in range(len(strokes)): 21 | stroke = strokes[j] 22 | val = driver(j * r, (j + 1) * r) * 10 23 | stroke.points.add(1, pressure=10, strength=val * 100) 24 | # stroke.points.add(1, pressure=val, strength=1) 25 | p = stroke.points[-1] 26 | p_old = stroke.points[-2] 27 | p.co = p_old.co 28 | p.co[0] = p_old.co[0] + math.sin(val) * .03 29 | i = random.randint(1, 2) 30 | p.co[i] = p_old.co[i] + math.cos(val) * .03 31 | while len(stroke.points) > max_values: 32 | stroke.points.pop(index=0) 33 | 34 | 35 | bpy.audvis.register_script("test", callback) 36 | -------------------------------------------------------------------------------- /doc/scripts/image-generator.py: -------------------------------------------------------------------------------- 1 | """ 2 | press ALT+P to run this script 3 | """ 4 | 5 | import math 6 | import bpy 7 | 8 | freq_per_pixel = 6.1 9 | size = [40, 40] # Don't put too high numbers here. You will regret it very soon! 10 | name = 'audvis' 11 | 12 | 13 | def callback(driver): 14 | if name not in bpy.data.images: 15 | img = bpy.data.images.new(name=name, width=size[0], height=size[1]) 16 | else: 17 | img = bpy.data.images[name] 18 | if list(img.size) != size: 19 | img.scale(size[0], size[1]) 20 | arr = [] 21 | for i in range(img.size[0] * img.size[1]): 22 | val = driver(i * freq_per_pixel, i * freq_per_pixel + freq_per_pixel * 3) * .5 23 | arr += [ 24 | val, 25 | val, 26 | val, 27 | 1 28 | ] 29 | img.pixels = arr 30 | 31 | 32 | bpy.audvis.register_script("test", callback) 33 | -------------------------------------------------------------------------------- /doc/scripts/mesh-grid.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | mesh = bpy.data.objects['Grid'].data 4 | freq_range = 10 5 | 6 | 7 | def callback(driver): 8 | for i in range(0, len(mesh.vertices)): 9 | frm = i * freq_range 10 | to = frm + freq_range 11 | val = driver(frm, to) / 15 12 | mesh.vertices[i].co[2] = val 13 | 14 | 15 | bpy.audvis.register_script("test", callback) 16 | -------------------------------------------------------------------------------- /doc/scripts/mesh-skin.py: -------------------------------------------------------------------------------- 1 | """ 2 | press ALT+P to run this script 3 | """ 4 | 5 | import bpy 6 | 7 | bpy.ops.curve.primitive_bezier_circle_add() 8 | circle = bpy.context.object 9 | 10 | bpy.ops.object.select_all(action='DESELECT') 11 | mesh = bpy.data.meshes.new("testing") 12 | obj = bpy.data.objects.new("testing", mesh) 13 | bpy.context.collection.objects.link(obj) 14 | bpy.context.view_layer.objects.active = obj 15 | bpy.ops.object.modifier_add(type='CURVE') 16 | obj.modifiers[0].object = circle 17 | bpy.ops.object.modifier_add(type='SKIN') 18 | 19 | mesh.vertices.add(1) 20 | mesh.skin_vertices[0].data[0].use_root = True 21 | 22 | 23 | def callback(driver): 24 | obj.data.vertices.add(1) 25 | v_old = obj.data.vertices[-2] 26 | v = obj.data.vertices[-1] 27 | 28 | v.co = (v_old.co[0] + .003, v_old.co[1] + .00015, v_old.co[2]) 29 | 30 | mesh.edges.add(1) 31 | mesh.edges[-1].vertices[0] = v_old.index 32 | mesh.edges[-1].vertices[1] = v.index 33 | 34 | skin_vert = obj.data.skin_vertices[0].data[-1] 35 | skin_vert.radius[0] = driver(10, 100) / 50 + .03 36 | skin_vert.radius[1] = driver(200, 300) / 50 + .03 37 | 38 | 39 | bpy.audvis.register_script("test", callback) 40 | -------------------------------------------------------------------------------- /doc/scripts/particles.py: -------------------------------------------------------------------------------- 1 | """ 2 | press ALT+P to run this script 3 | """ 4 | 5 | import bpy 6 | import bpy 7 | import math 8 | 9 | object = bpy.data.objects['Cube'] 10 | 11 | def deps_part_system(): 12 | degp = bpy.context.evaluated_depsgraph_get() 13 | return object.evaluated_get(degp).particle_systems[0] 14 | 15 | 16 | def callback(driver): 17 | particle_system = deps_part_system() 18 | particles = particle_system.particles 19 | 20 | scene = bpy.context.scene 21 | curframe = scene.frame_current_final 22 | fps = scene.render.fps / scene.render.fps_base 23 | 24 | # at start-frame, clear the particle cache 25 | if curframe == scene.frame_start: 26 | object.particle_systems[0].seed += 0 27 | 28 | i = 0 29 | cnt = 0 30 | for part in particles: 31 | i += 1 32 | 33 | part.birth_time = bpy.context.scene.frame_start 34 | part.die_time = bpy.context.scene.frame_end 35 | if part.location[2] < -1: 36 | part.location[2] = 2 37 | if part.alive_state == 'ALIVE': 38 | cnt += 1 39 | j = (i * 3) % 2000 40 | dist = math.log(driver(j, j + 10) + 1) * .5 + 1 41 | # dist = (driver(j, j + 10) * .1) + 1 42 | z = i / 10 - curframe / fps / 5 + driver(j, j+20) * .3 43 | print("z", z, math.cosh(1 - z * .01)) 44 | part.location = ( 45 | math.cos(i - curframe / fps) * dist * max(0, 1-abs(z)*.02), 46 | math.sin(i - curframe / fps) * dist * max(0, 1-abs(z)*.02), 47 | z % 4 - 2, 48 | ) 49 | part.velocity = (0, 0, 0) 50 | elif part.alive_state == 'UNBORN': 51 | pass 52 | else: 53 | print(part.alive_state, part.die_time) 54 | 55 | print(cnt) 56 | 57 | 58 | 59 | bpy.audvis.register_script("test", callback) 60 | -------------------------------------------------------------------------------- /doc/scripts/suzanne.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import math 3 | 4 | freq_range = 10 5 | 6 | if 'Suzanne' in bpy.data.objects: 7 | obj = bpy.data.objects['Suzanne'] 8 | bpy.data.objects.remove(obj) 9 | bpy.ops.mesh.primitive_monkey_add(size=3.5) 10 | obj = bpy.context.object 11 | obj.rotation_euler[2] = .5 12 | mesh = obj.data 13 | orig = [] 14 | normals = [] 15 | for v in mesh.vertices: 16 | orig.append(v.co.to_tuple()) 17 | n = v.co.copy() 18 | n.normalize() 19 | normals.append(n) 20 | 21 | 22 | def callback(driver): 23 | for i in range(len(mesh.vertices)): 24 | frm = i * freq_range 25 | to = frm + freq_range 26 | val = math.log(driver(frm, to) * .3 + 1) / 4 27 | o = orig[i] 28 | n = normals[i] 29 | mesh.vertices[i].co = ( 30 | o[0] + (n[0]) * val, 31 | o[1] + (n[1]) * val, 32 | o[2] + (n[2]) * val, 33 | ) 34 | 35 | 36 | bpy.audvis.register_script("test", callback) 37 | -------------------------------------------------------------------------------- /doc/sequence.md: -------------------------------------------------------------------------------- 1 | # Sequence Analyzer 2 | 3 | Sequence analyzer is similar to [Realtime Analyzer](./realtime.md), but uses sound sequences in Blender's Video Sequence 4 | Editor. Don't worry if you don't like the VSE, there is a quite good user interface inside AudVis "Sequence Analyzer" 5 | panel. 6 | 7 | ## Settings 8 | 9 | - **Enable** (in the Sequence Analyzer panel header): do you want to use Sequence Analyzer? If not, keep this disabled 10 | - **Enable** single sequence - you can enable/disable every single sound sequence for use in AudVis Sequence Analyzer 11 | - **Add Sound Sequence** - choose a sound file to use. You can use multiple sound files, and animate different things by 12 | different sound files 13 | - **Remove Sound Sequence** - remove selected sound sequence 14 | - **Name** - name of the sequence. You can change it or copy the name to use it in the [driver](./drivers.md) expression 15 | - **Start Frame** - this is important! This is the frame when the sound sequence starts to play and also AudVis starts 16 | to parse it 17 | - **Start Offset** - just offset - you can cut out the start of the song for example 18 | - **Length** - just length - you can cut out the end of the song for example 19 | - **Align End Frame by Sequences** - set current scene's End Frame to the last frame of all sound sequences - if your 20 | song is 1234 frames long and starts at frame 1, end frame of the scene will be set to 1235 21 | -------------------------------------------------------------------------------- /doc/shape-modifier.md: -------------------------------------------------------------------------------- 1 | # Shape Modifier 2 | 3 | Shape modifier is used to animate all vertices / points of meshes, curves, surfaces, grease pencils or lattices. Animate 4 | by sound (or midi), of course. 5 | 6 | Internally, shape modifier calls the same function like the one used for [driver expressions](./drivers.md) many times. 7 | 8 | In case of mesh, curve, surface or lattice objects, two Shape Keys are created - one named "AudVis Shape Origin" and the 9 | second one "AudVis Shape Target". For every frame, relevant data is copied from "AudVis Shape Origin" into "AudVis Shape 10 | Target", and modified the way we want. 11 | 12 | In case of grease pencil objects, there are two frames used - frame 0, which will be copied and modified by sound into 13 | frame 1. **Warning**: if you have animated grease pencil with data on multiple frames, AudVis will damage it and keep 14 | only one frame from your original data. 15 | 16 | ## Settings 17 | 18 | - **Enable** - (in the Shape Modifier panel header): do you want to use Shape Modifiers? If not, keep this disabled 19 | - **Enable** for single selected object - every object of supported type is animated separately if enabled 20 | - **Animation Type** 21 | - **Normal** - in reality, object normal. Move vertices / points away from center of the object. Best in case of 22 | circles or spheres. 23 | - **Location Z** - moves vertices / points along the Z axis. Best for use with grid mesh 24 | - **Location** - additional setting **Vector** appears. Here you can setup the direction the points will move 25 | - **Track to object** - move all vertices / points towards the center of a selected object in the **Target** 26 | property 27 | - **Vertex Group Weight** - creates two vertex groups - "AudVis Origin" and "AudVis Target". You can then use the 28 | "AudVis Target" vertex group for example in the Displace Modifier, or in geometry nodes, or almost anywhere you 29 | want. 30 | - **UV Map** - creates two UV maps - "audvis from" and "audvis to" and moves the points in the map. Not sure if this 31 | is ever useful 32 | - **Vertex Color** - creates two Vertex Colors - "audvis from" and "audvis to". You can use the "audvis to" in the 33 | shading nodes, or geometry nodes 34 | - **Curve Radius** (only for curve objects) - useful for example when you have `Geometry -> Bevel -> Depth`, but 35 | also in some other cases 36 | - **Curve Tilt** (only for curve objects) - useful for example if you have `Geometry -> Extrude` 37 | or `Geometry -> Bevel -> Object` 38 | - **Operation Type** 39 | - **Add** - adds value to existing value in "audvis from" / "AudVis Origin" 40 | - **Set** - just sets the value received from the analyzer 41 | - **Multiple Freq properties** - the whole concept is [described here](./freq-sequencing.md) 42 | - **Factor** - multiply received values by this number 43 | - **Add** - add this number to received values 44 | - **Order** - if you want to see vertices order, go to Blender preferences, enable `Interface -> Developer Extras` and 45 | then in the 3D Viewport enable `Overlays menu -> Developer -> Indices` 46 | - **1,2,3...** - ascending order 47 | - **...3,2,1** - descending order 48 | - **random** - random order 49 | - **Use Vertex Group** (only for mesh objects) - if enabled, a new vertex group is created - "AudVis Location Weight" 50 | (not the best name for this use, sorry...). Vertices removed from this vertex group will be skipped. Other vertices 51 | will be affected by shape modifier by the weight set in this "AudVis Location Weight" vertex group. If a weight of a 52 | vertex is 0.2, the value will be multiplied by 0.2. Of course, you can use Weight Paint. 53 | - **Bake Shape Modifier** - the whole animation will be baked into animation data, so you can render your animation on a 54 | render farm or different computer where you don't have AudVis installed. 55 | - **Clean Data** - deleted all the relevant shape keys, vertex groups and vertex colors -------------------------------------------------------------------------------- /doc/spectrogram.md: -------------------------------------------------------------------------------- 1 | # Spectrogram 2 | 3 | A spectrogram is a visual representation of the spectrum of frequencies of a signal as it varies with 4 | time ([Wiki](https://en.wikipedia.org/wiki/Spectrogram)). In Blender, it's really powerful tool. You can use it in 5 | Geometry Nodes, in old school Textures, in shading nodes... I'm not even able to name a fraction of the potential uses. 6 | 7 | ## Settings 8 | 9 | - **Multiple Freq properties** - the whole concept is [described here](./freq-sequencing.md) 10 | - **Mode** 11 | - **Rolling** - for every frame, the whole image is moved 1 pixel up (upmost pixel is lost), and then current data 12 | will be drawn into the bottom 1px line 13 | - **One Image** - the height of the image will be the same as your scene's duration - if it's 1000 frames, the image 14 | will be 1000 pixels high. Every line of image represents one frame. The main advantage is that you have only 1 15 | image and you don't need to handle baked image sequences. 16 | - **Snapshot** - all pixels will be filled by current data 17 | - **Image Width** - if the image's width isn't the same as this value, it will be resized 18 | - **Image Height** (only if Rolling or Snapshot Mode is selected) - if the image's height isn't the same as this value, 19 | it will be resized. **Warning!** Snapshot mode with high width+height values will result in poor performance. 100x100 20 | pixels in snapshot mode means 10,000 calls for data, which is more than you want and need 21 | - **Factor RGB** - multiply returned point value by these values. If you put 3 same values, the result image will be 22 | black and white. If not, it will be more colorful 23 | - **Skip Frames** - if higher than 0, lower number of frames will be drawn. 1 means every second frame, 2 means every 24 | third frame will be drawn. 25 | - **Clear on First Frame** - if **One Image** or **Rolling** mode is selected, the image will be cleared on the frame 26 | 1 (or whatever frame is your Start Frame) 27 | - **Base Color** - this will be background color and will be added to the animated values 28 | - **Bake Path** - path to the directory where you want to bake spectrogram images 29 | - **Bake Spectrogram** - perform the baking. In case of **One Image** mode, only one image will be saved. In other 30 | cases, images named "audvis-spect-0001.png", "audvis-spect-0002.png"... will be saved to selected **Bake Path** -------------------------------------------------------------------------------- /doc/spread-the-drivers.md: -------------------------------------------------------------------------------- 1 | # Spread the Drivers 2 | 3 | If you right click on an animatable property, you should see an option on the last position in the 4 | menu: `Audvis: Spread the Drivers`. There is also a panel named "Spread the Drivers" where you can see what is happening 5 | and where you can tweak things. 6 | 7 | # Settings 8 | 9 | - **Multiple Freq properties** - the whole concept is [described here](./freq-sequencing.md) 10 | - **Expression** - you can write your own expression and string "audvis()" will be replaced by the generated expression. 11 | If you put there `clamp(audvis() * 2.5, 0, 10)`, the result expression will be something like `clamp(audvis(50, 100) * 2.5, 0, 10)` -------------------------------------------------------------------------------- /doc/upbge.md: -------------------------------------------------------------------------------- 1 | # UPBGE 2 | 3 | You can use AudVis Real Time Analyzer and Midi Realtime in UPBGE. 4 | 5 | In the N-panel of the Logic Bricks Editor, and in the Game Object Properties, there are some buttons for AudVis. After 6 | creating a component, you can set its properties in the Game Object Properties (icon just over Material Properties). 7 | 8 | - **Logic setup**: 9 | - **Create Action Logic** - generates connected Sensor, Controller and Actuator. After doing this, you have to setup 10 | the created Action - set the Property and Action 11 | - **Register Component**: 12 | - **Updater** - you need at least one AudVis call per every frame. If you only want to use the Shape Modifier or 13 | Spectrogram, use this component 14 | - **Realtime Sound** - creates a game property and periodically sets the value by the current sound frequencies 15 | - **freq_from** and **freq_to** - look for [driver expressions](./drivers.md) 16 | - **factor** - multiply the result value 17 | - **add** - add to the result value 18 | - **Midi Realtime** - creates a game property and periodically sets the value by the current midi state 19 | - **note** - MIDI note which you want to react to 20 | - **factor** - multiply the result value 21 | - **add** - add to the result value -------------------------------------------------------------------------------- /doc/video-capture.md: -------------------------------------------------------------------------------- 1 | # Video Capture 2 | 3 | Use only with Realtime Analyzer. Reads images from your webcam with opencv and updates the **Output Image**. 4 | 5 | **Warning!** Because some api issues, audvis first saves the captured image to your HDD or SSD and then loads it into 6 | Blender. This brings performance issues and also this 7 | can [wear out your SSD](https://www.dell.com/support/kbdoc/en-us/000137999/hard-drive-why-do-solid-state-devices-ssd-wear-out?lang=en) 8 | . If you are running Blender on Linux, you can 9 | setup [tmpfs](https://www.kernel.org/doc/html/latest/filesystems/tmpfs.html) and set the **Temp Path** property to a 10 | directory inside tmpfs partition. On Windows, search for ImDisk or RamDisk. You don't need a lot of space. 100MB is more 11 | than enough. It looks MacOs doesn't have 12 | 13 | If you are using MacOs, you need to run Blender manually from Terminal to ask for permissions. Open Terminal and 14 | run `/Applications/Blender.app/Contents/MacOS/blender` 15 | 16 | ## Settings 17 | 18 | - **Camera Index** - if you have multiple cameras, choose which one you want to use 19 | - **Temp Path** - where to periodically save temporary image. If empty, temporary directory is created internally -------------------------------------------------------------------------------- /note_calculator.py: -------------------------------------------------------------------------------- 1 | a = 2 ** (1 / 12) 2 | 3 | 4 | def calculate_note(distance_from_a4, a4=440): 5 | return a4 * (a ** distance_from_a4) 6 | -------------------------------------------------------------------------------- /requirements-nodeps.txt: -------------------------------------------------------------------------------- 1 | opencv-python==4.10.0.84 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | cffi==1.17.1 2 | pycparser==2.22 3 | pygame==2.6.0 4 | sounddevice==0.5.0 5 | soundfile==0.12.1 6 | mido==1.3.2 7 | -------------------------------------------------------------------------------- /scripting.py: -------------------------------------------------------------------------------- 1 | class Scripting: 2 | callbacks = {} 3 | 4 | def run(self, audvis): 5 | for i in self.callbacks: 6 | self.callbacks[i](audvis.driver) 7 | 8 | def register(self, name, callback): 9 | self.callbacks[name] = callback 10 | 11 | def reset(self): 12 | self.callbacks = {} 13 | 14 | 15 | classes = [] 16 | -------------------------------------------------------------------------------- /switchscenes.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | 4 | def try_switch_scenes(current_scene): 5 | if not current_scene.audvis.realtime_enable: 6 | return False 7 | if not current_scene.audvis.realtime_switchscenes: 8 | return False 9 | if current_scene.frame_current == current_scene.frame_start: 10 | return False 11 | if current_scene.frame_current != current_scene.frame_end: 12 | return False 13 | if current_scene.frame_start == current_scene.frame_end: 14 | return False 15 | if len(bpy.data.scenes) < 2: 16 | return False 17 | if not hasattr(bpy.context.window, 'scene'): # while rendering, there is not window thankfully 18 | return False 19 | if not bpy.context.screen.is_animation_playing: 20 | return False 21 | try: 22 | for particle_settings in bpy.data.particles: 23 | particle_settings.frame_start = particle_settings.frame_start # clear cache - really ugly way 24 | except Exception as e: 25 | # print(e) 26 | pass 27 | use_next = False 28 | set_this_scene = None 29 | for s in bpy.data.scenes: 30 | if use_next: 31 | set_this_scene = s 32 | break 33 | elif s is current_scene: 34 | use_next = True 35 | if set_this_scene is None: 36 | set_this_scene = bpy.data.scenes[0] 37 | _skip_to_start(current_scene) 38 | _skip_to_start(set_this_scene) 39 | bpy.context.window.scene = set_this_scene 40 | # override for a bug: animation stuck switching frames 1 / 2 / 1 / 2 / 1 / 2... forever: 41 | bpy.ops.screen.animation_play() # pause 42 | bpy.ops.screen.animation_play() # play 43 | # end override 44 | return True 45 | 46 | 47 | def _skip_to_start(scene): 48 | scene.frame_current = scene.frame_start 49 | -------------------------------------------------------------------------------- /ui/__init__.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | from . import ( 4 | realtime, 5 | generator, 6 | sequence, 7 | partymode, 8 | scripttemplates, 9 | force_reload, 10 | values, 11 | video, 12 | shapemodifier, 13 | global_settings, 14 | drivers_bake, 15 | spectrogram, 16 | animation_nodes, 17 | preferences, 18 | armature_generator, 19 | install_ui, 20 | props, 21 | midi, 22 | spread_drivers, 23 | bge, 24 | daw_arrangement 25 | ) 26 | from .buttonspanel import AudVisButtonsPanel_Npanel 27 | 28 | 29 | class AUDVIS_PT_audvisNpanel(AudVisButtonsPanel_Npanel): 30 | bl_label = "AudVis - Audio Visualizer" 31 | 32 | @classmethod 33 | def poll(cls, context): 34 | return True 35 | 36 | def draw(self, context): 37 | layout = self.layout 38 | 39 | col = layout.column(align=True) 40 | row = col.row() 41 | row.label(text="Sync Mode:") 42 | row.prop(context.scene, "sync_mode", text="", text_ctxt="Set AV-sync if your sound is out of sync while playing") 43 | 44 | col = layout.column(align=True) 45 | col.prop(context.scene.audvis, "sample") 46 | col.prop(context.scene.audvis, "subframes") 47 | col.prop(context.scene.audvis, "channels") 48 | col.prop(context.scene.audvis, "default_channel_sound") 49 | row = col.row() 50 | row.label(text="Default MIDI Channel") 51 | row.prop(context.scene.audvis, "default_channel_midi", text="") 52 | 53 | col = layout.column(align=True) 54 | col.operator("audvis.forcereload", text="Reload AudVis", icon='FILE_REFRESH') 55 | 56 | 57 | class AUDVIS_OT_copyString(bpy.types.Operator): 58 | bl_idname = "audvis.copy_string" 59 | bl_label = "Copy to Clipboard" 60 | 61 | value: bpy.props.StringProperty() 62 | 63 | def execute(self, context): 64 | context.window_manager.clipboard = self.value 65 | return {"FINISHED"} 66 | 67 | 68 | class AudvisWindowProperties(bpy.types.PropertyGroup): 69 | ispartymode: bpy.props.BoolProperty(name="Is Party Mode Window", default=False) 70 | 71 | 72 | def register(): 73 | scripttemplates.register() 74 | partymode.register() 75 | spread_drivers.register() 76 | bpy.types.Scene.audvis = bpy.props.PointerProperty(type=props.scene.AudvisSceneProperties) 77 | bpy.types.Object.audvis = bpy.props.PointerProperty(type=props.obj.AudvisObjectProperties) 78 | if hasattr(bpy.types, "SoundSequence"): 79 | bpy.types.SoundSequence.audvis = bpy.props.PointerProperty(type=props.sequence.AudvisSequenceProperties) 80 | else: 81 | bpy.types.SoundStrip.audvis = bpy.props.PointerProperty(type=props.sequence.AudvisSequenceProperties) 82 | bpy.types.Window.audvis = bpy.props.PointerProperty(type=AudvisWindowProperties) 83 | preferences.on_npanelname_update(bpy.context.preferences.addons[bpy.audvis._module_name].preferences, bpy.context) 84 | 85 | 86 | def unregister(): 87 | scripttemplates.unregister() 88 | partymode.unregister() 89 | spread_drivers.unregister() 90 | realtime.unregister() 91 | video.unregister() 92 | del bpy.types.Scene.audvis 93 | del bpy.types.Object.audvis 94 | if hasattr(bpy.types, "SoundSequence"): 95 | del bpy.types.SoundSequence.audvis 96 | else: 97 | del bpy.types.SoundStrip.audvis 98 | del bpy.types.Window.audvis 99 | 100 | 101 | def on_blendfile_loaded(): 102 | video.on_blendfile_loaded() 103 | 104 | 105 | def on_blendfile_save(): 106 | video.on_blendfile_save() 107 | 108 | 109 | classes = [ 110 | AUDVIS_OT_copyString, 111 | AUDVIS_PT_audvisNpanel, 112 | AudvisWindowProperties, 113 | ] \ 114 | + props.classes \ 115 | + values.classes \ 116 | + sequence.classes \ 117 | + realtime.classes \ 118 | + midi.classes \ 119 | + partymode.classes \ 120 | + video.classes \ 121 | + shapemodifier.classes \ 122 | + armature_generator.classes \ 123 | + generator.classes \ 124 | + scripttemplates.classes \ 125 | + force_reload.classes \ 126 | + spectrogram.classes \ 127 | + drivers_bake.classes \ 128 | + animation_nodes.classes \ 129 | + spread_drivers.classes \ 130 | + install_ui.classes \ 131 | + preferences.classes \ 132 | + bge.classes \ 133 | + daw_arrangement.classes \ 134 | + [] 135 | -------------------------------------------------------------------------------- /ui/animation_nodes/__init__.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | from . import (expression, script) 4 | 5 | 6 | class AUDVIS_PT_animationnodes(bpy.types.Panel): 7 | bl_space_type = 'NODE_EDITOR' 8 | bl_region_type = 'UI' 9 | bl_category = "AudVis" 10 | # bl_options = {'DEFAULT_CLOSED'} 11 | bl_label = 'AudVis' 12 | 13 | @classmethod 14 | def poll(cls, context): 15 | if not hasattr(context, "getActiveAnimationNodeTree"): 16 | return False 17 | if context.getActiveAnimationNodeTree() is None: 18 | return False 19 | return True 20 | 21 | def draw(self, context): 22 | col = self.layout.column(align=True) 23 | col.prop(context.scene.audvis.animation_nodes, "type") 24 | col.operator(AUDVIS_OT_animationnodesMakeNodes.bl_idname) 25 | if bpy.ops.audvis.animationnodes_fixparticlesdataoutputs.poll(): 26 | col = self.layout.column(align=True) 27 | col.operator(bpy.ops.audvis.animationnodes_fixparticlesdataoutputs.bl_idname) 28 | 29 | 30 | class AUDVIS_OT_animationnodesMakeNodes(bpy.types.Operator): 31 | bl_label = "Create AudVis Script Node" 32 | bl_idname = "audvis.animationnodes_makenodes" 33 | 34 | @classmethod 35 | def poll(cls, context): 36 | return AUDVIS_PT_animationnodes.poll(context) 37 | 38 | def _unselect_all(self, node_tree): 39 | for node in node_tree.nodes: 40 | node.select = False 41 | 42 | def execute(self, context): 43 | node_tree = context.getActiveAnimationNodeTree() 44 | self._unselect_all(node_tree) 45 | type = context.scene.audvis.animation_nodes.type 46 | if type == 'script': 47 | script.make_nodes(context) 48 | elif type == 'expression': 49 | expression.make_nodes(context) 50 | bpy.ops.node.translate_attach('INVOKE_DEFAULT') 51 | return {'FINISHED'} 52 | 53 | 54 | classes = [ 55 | AUDVIS_PT_animationnodes, 56 | AUDVIS_OT_animationnodesMakeNodes, 57 | ] 58 | -------------------------------------------------------------------------------- /ui/animation_nodes/expression.py: -------------------------------------------------------------------------------- 1 | def _input(node, name, default_value, type="an_FloatSocket"): 2 | node.newInputSocket(type) 3 | socket = node.inputs[-2] # TODO: find better way, -2 is very bad 4 | socket.name = name 5 | socket.text = name 6 | socket.value = default_value 7 | return socket 8 | 9 | 10 | def _inputNode(node, name, default_value, index): 11 | sock = _input(node, name, default_value) 12 | input_node = node.nodeTree.nodes.new(type='an_DataInputNode') 13 | input_node.outputs[0].linkWith(sock) 14 | input_node.location[0] -= 200 15 | input_node.location[1] -= index * 80 16 | input_node.inputs[0].value = default_value 17 | return sock 18 | 19 | 20 | def make_nodes(context): 21 | node_tree = context.getActiveAnimationNodeTree() 22 | expr_node = node_tree.nodes.new(type="an_ExpressionNode") 23 | expr_node.expression = "bpy.app.driver_namespace['audvis'](freq_from, freq_to) * factor" 24 | 25 | _inputNode(expr_node, 'freq_from', default_value=0, index=0) 26 | _inputNode(expr_node, 'freq_to', default_value=100, index=1) 27 | _inputNode(expr_node, 'factor', default_value=1, index=2) 28 | 29 | # expr_node.outputs[0].dataType = "an_FloatSocket" 30 | -------------------------------------------------------------------------------- /ui/animation_nodes/script.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | def _input(node, name, default_value, type="an_FloatSocket"): 4 | node.newInputSocket(type) 5 | socket = node.inputs[-2] # TODO: find better way, -2 is very bad 6 | socket.text = name 7 | socket.value = default_value 8 | return socket 9 | 10 | 11 | def make_nodes(context): 12 | node_tree = context.getActiveAnimationNodeTree() 13 | text_block = bpy.data.texts.new(name="audvis-an.py") 14 | text_block.write(_create_script(context)) 15 | script = node_tree.nodes.new(type="an_ScriptNode") 16 | script.textBlock = text_block 17 | script.subprogramName = "audvis" 18 | script.label = "AudVis Script" 19 | script.newOutputSocket("an_FloatSocket") 20 | output_socket = script.outputs[-2] 21 | output_socket.text = "out" 22 | 23 | _input(node=script, name="index", default_value=0, type="an_IntegerSocket") 24 | _input(node=script, name="freq_start", default_value=0) 25 | _input(node=script, name="freq_range", default_value=50) 26 | _input(node=script, name="freq_step", default_value=50) 27 | _input(node=script, name="factor", default_value=1) 28 | 29 | subprogram = node_tree.nodes.new(type="an_InvokeSubprogramNode") 30 | subprogram.subprogramIdentifier = script.identifier 31 | subprogram.location[1] += 300 32 | subprogram.label = "AudVis Subprogram" 33 | 34 | 35 | def _create_script(context): 36 | text = """ 37 | driver = bpy.app.driver_namespace['audvis'] 38 | out = factor * (driver(index * freq_step + freq_start, index * freq_step + freq_range + freq_start)) 39 | """ 40 | return text 41 | -------------------------------------------------------------------------------- /ui/armature_generator/__init__.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | from . import generate 4 | from .. import ui_lib 5 | from ..buttonspanel import AudVisButtonsPanel_Npanel 6 | from ..hz_label import hz_label, notes_label 7 | 8 | 9 | def calc_freq_step(self): 10 | if self.freq_step_enable: 11 | return self.freq_step 12 | return self.freqrange 13 | 14 | 15 | class AUDVIS_PT_ArmatureGeneratorNpanel(AudVisButtonsPanel_Npanel): 16 | bl_label = "Generate Armature" 17 | 18 | @classmethod 19 | def poll(cls, context): 20 | return True 21 | 22 | def draw(self, context): 23 | layout = self.layout 24 | obj = context.active_object or context.object 25 | col = layout.column(align=True) 26 | if not (obj and obj.type == 'MESH'): 27 | col.label(text="Select a mesh object") 28 | return 29 | col.label(text="Selected faces will be used") 30 | props = obj.audvis.armature_generator 31 | col.prop(props, "inset") 32 | if props.inset: 33 | col.label(text="Warning: Don't use this") 34 | col.label(text="option repeatedly") 35 | col.prop(props, "inset_and_extrude") 36 | col.prop(props, "inset_size") 37 | col = layout.column(align=True) 38 | col.prop(props, "factor") 39 | col = layout.column(align=True) 40 | 41 | col.prop(props, "freq_seq_type") 42 | if props.freq_seq_type == "classic": 43 | col.prop(props, "freqrange") 44 | col.prop(props, "freqstart") 45 | col.prop(props, "freq_step_enable") 46 | if props.freq_step_enable: 47 | col.prop(props, "freq_step") 48 | elif props.freq_seq_type == "notes": 49 | col.prop(props, "note_a4_freq") 50 | col.prop(props, "note_step") 51 | col.prop(props, "note_offset") 52 | elif props.freq_seq_type == "midi": 53 | col.prop(props.midi, "offset") 54 | count = len([1 for p in obj.data.polygons if p.select]) 55 | col.label(text="Toggle edit mode to update") 56 | col.label(text="the number of selected faces") 57 | if count: 58 | if props.freq_seq_type == 'midi': 59 | whole_range_text = None 60 | elif props.freq_seq_type == 'notes': 61 | whole_range_text = notes_label(count, props.note_step, props.note_a4_freq, props.note_offset) 62 | else: 63 | whole_range_text = hz_label(start=props.freqstart, 64 | range_per_point=props.freqrange, 65 | step=calc_freq_step(props), 66 | points_count=count) 67 | if whole_range_text is not None: 68 | col.label(text=whole_range_text) 69 | if props.freq_seq_type == 'midi': 70 | ui_lib.generators_ui_midi(self, context, props.midi) 71 | else: 72 | ui_lib.generators_ui_sequence(self, context, props) 73 | col = layout.column(align=False) 74 | col.prop(props, "keep_old_vgroups") 75 | col.operator("audvis.generate_armature") 76 | 77 | 78 | class AUDVIS_OT_Generate(bpy.types.Operator): 79 | bl_idname = "audvis.generate_armature" 80 | bl_label = "Generate AudVis Armature" 81 | 82 | @classmethod 83 | def poll(cls, context): 84 | if context.active_object or context.object: 85 | return True 86 | return False 87 | 88 | def execute(self, context): 89 | generate.generate(context) 90 | return {'FINISHED'} 91 | 92 | 93 | classes = [ 94 | AUDVIS_PT_ArmatureGeneratorNpanel, 95 | AUDVIS_OT_Generate, 96 | ] 97 | -------------------------------------------------------------------------------- /ui/armature_generator/generate.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | from ...note_calculator import calculate_note 4 | 5 | 6 | def generate(context): 7 | obj = context.active_object or context.object 8 | props = obj.audvis.armature_generator 9 | if props.inset: 10 | bpy.ops.object.mode_set(mode='EDIT', toggle=False) 11 | bpy.ops.mesh.inset(thickness=props.inset_size, depth=0, use_individual=True) 12 | if props.inset_and_extrude: 13 | bpy.ops.mesh.extrude_region() 14 | props.inset = False 15 | bpy.ops.object.mode_set(mode='OBJECT', toggle=False) 16 | l = [p for p in obj.data.polygons if p.select] 17 | 18 | if not props.keep_old_vgroups: 19 | for g in obj.vertex_groups: 20 | if g.name.startswith("AudvisArm"): 21 | obj.vertex_groups.remove(g) 22 | 23 | bones_conf = [] 24 | for p in l: 25 | vg = obj.vertex_groups.new(name="AudvisArm") 26 | vg.add(index=p.vertices, weight=1, type='ADD') 27 | bones_conf.append({ 28 | "name": vg.name, 29 | "center": p.center, 30 | "normal": p.normal, 31 | }) 32 | 33 | if props.armature_object and props.armature_object.type == 'ARMATURE' and props.armature_object.name in context.scene.objects: 34 | arm_object = props.armature_object 35 | bpy.context.view_layer.objects.active = arm_object 36 | bpy.ops.object.mode_set(mode='EDIT', toggle=False) 37 | if not props.keep_old_vgroups: 38 | for b in list(arm_object.data.edit_bones): 39 | arm_object.data.edit_bones.remove(b) 40 | else: 41 | bpy.ops.object.add(type="ARMATURE", enter_editmode=True) 42 | arm_object = context.object 43 | arm_object.location = (0, 0, 0) 44 | arm_object.parent = obj 45 | props.armature_object = arm_object 46 | # arm_object.location = obj.location 47 | # arm_object.rotation_euler = obj.rotation_euler 48 | # arm_object.scale = obj.scale 49 | 50 | for bone_conf in bones_conf: 51 | bone = arm_object.data.edit_bones.new(name=bone_conf["name"]) 52 | bone.head = bone_conf["center"] 53 | bone.tail = bone_conf["center"] + bone_conf["normal"] / 3 54 | # arm_object.pose.bones[bone.name] 55 | 56 | modifier_name = "AudVis Armature" 57 | if not props.keep_old_vgroups and modifier_name in obj.modifiers: 58 | modifier = obj.modifiers[modifier_name] 59 | else: 60 | modifier = obj.modifiers.new(name=modifier_name, type="ARMATURE") 61 | modifier.object = arm_object 62 | bpy.ops.object.mode_set(mode='OBJECT', toggle=False) 63 | 64 | index = 0 65 | rangeperobject = props.freqrange 66 | freqstart = props.freqstart 67 | if props.freq_step_enable: 68 | freq_step = props.freq_step 69 | else: 70 | freq_step = rangeperobject 71 | for bone_conf in bones_conf: 72 | pose_bone = arm_object.pose.bones[bone_conf["name"]] 73 | fcurve = pose_bone.driver_add("location", 1) 74 | if props.freq_seq_type == 'midi': 75 | expr_kwargs = 'midi={}'.format(index + props.midi.offset) 76 | if props.midi.file != '': 77 | expr_kwargs += ', file=' + repr(props.midi.file) 78 | if props.midi.track != '': 79 | expr_kwargs += ', track=' + repr(props.midi.track) 80 | if props.midi.channel != 'all' and props.midi.channel != '': 81 | expr_kwargs += ', ch=' + repr(props.midi.channel) 82 | if props.midi.device != '': 83 | expr_kwargs += ', device=' + repr(props.midi.device) 84 | fcurve.driver.expression = "audvis({}) * {:.6}".format(expr_kwargs, props.factor) 85 | else: 86 | expr_kwargs = '' 87 | if props.freq_seq_type == 'notes': 88 | freq_from = calculate_note((index + props.note_offset) * props.note_step, 89 | props.note_a4_freq) 90 | freq_to = calculate_note((index + props.note_offset + 1) * props.note_step, 91 | props.note_a4_freq) 92 | freq_from = round(freq_from * 100) / 100 93 | freq_to = round(freq_to * 100) / 100 94 | else: 95 | freq_from = index * freq_step + freqstart 96 | freq_to = freq_from + rangeperobject 97 | if props.sound_sequence != '': 98 | expr_kwargs += ', seq=' + repr(props.sound_sequence) 99 | if props.sequence_channel > 0: 100 | expr_kwargs +=', seq_channel=' + str(props.sequence_channel) 101 | if props.channel != 1: 102 | expr_kwargs += ', ch={}'.format(props.channel) 103 | fcurve.driver.expression = "audvis({}, {}{}) * {:.6}".format(freq_from, freq_to, expr_kwargs, props.factor) 104 | index += 1 105 | bpy.context.view_layer.objects.active = obj 106 | -------------------------------------------------------------------------------- /ui/bge/__init__.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.types import Panel 3 | 4 | from . import bge_logic_create, bge_register_component 5 | 6 | options = [ 7 | ('audvis.bge.Updater', "Updater", "Only updates (if you don't use any other AudVis components anywhere)"), 8 | ('audvis.bge.Realtime', "Realtime Sound", "Set property value from realtime sound driver"), 9 | ('audvis.bge.MidiRealtime', "Midi Realtime", "Set property value from realtime sound driver"), 10 | ] 11 | 12 | 13 | class AUDVIS_PT_Bge(Panel): 14 | bl_space_type = 'LOGIC_EDITOR' 15 | bl_region_type = 'UI' 16 | bl_category = "Logic" 17 | bl_label = "AudVis" 18 | 19 | @classmethod 20 | def poll(cls, context): 21 | return len(dir(bpy.ops.logic)) 22 | 23 | def draw(self, context): 24 | box = self.layout.box() 25 | box.label(text="Logic Setup:") 26 | box.operator('audvis.bge_logic_create') 27 | 28 | box = self.layout.box() 29 | box.label(text="Register Component:") 30 | for item in options: 31 | col = box.column() 32 | col.label(text=item[2]) 33 | col.operator('audvis.bge_register_component', text=item[1]).component = item[0] 34 | 35 | 36 | class AUDVIS_PT_BgeProps(AUDVIS_PT_Bge): 37 | bl_space_type = 'PROPERTIES' 38 | bl_region_type = 'WINDOW' 39 | bl_category = "game" 40 | bl_label = "AudVis" 41 | bl_order = 3000000 # move to lower positions, under the default panels 42 | 43 | 44 | classes = [ 45 | bge_register_component.AUDVIS_OT_BgeRegisterComponent, 46 | bge_logic_create.AUDVIS_OT_BgeLogicCreate, 47 | AUDVIS_PT_Bge, 48 | AUDVIS_PT_BgeProps, 49 | ] 50 | -------------------------------------------------------------------------------- /ui/bge/bge_logic_create.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | 4 | class AUDVIS_OT_BgeLogicCreate(bpy.types.Operator): 5 | bl_idname = "audvis.bge_logic_create" 6 | bl_label = "Create Action Logic" 7 | 8 | @classmethod 9 | def poll(cls, context): 10 | return len(dir(bpy.ops.logic)) \ 11 | and bpy.ops.logic.sensor_add.poll() 12 | 13 | def execute(self, context): 14 | obj = context.object 15 | bpy.ops.logic.sensor_add(type='ALWAYS', name="always audvis") 16 | sensor = obj.game.sensors[-1] 17 | sensor.use_pulse_true_level = True 18 | 19 | bpy.ops.logic.controller_add(type='LOGIC_AND', name="and audvis", object="") 20 | controller = obj.game.controllers[-1] 21 | 22 | bpy.ops.logic.actuator_add(type='ACTION', name="action audvis") 23 | actuator = context.object.game.actuators[-1] 24 | 25 | controller.link(sensor=sensor) 26 | controller.link(actuator=actuator) 27 | if obj.animation_data: 28 | actuator.action = obj.animation_data.action 29 | actuator.play_mode = 'PROPERTY' 30 | 31 | return {"FINISHED"} 32 | -------------------------------------------------------------------------------- /ui/bge/bge_register_component.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | 4 | class AUDVIS_OT_BgeRegisterComponent(bpy.types.Operator): 5 | bl_idname = "audvis.bge_register_component" 6 | bl_label = "Register AudVis Component" 7 | 8 | component: bpy.props.StringProperty(name="Component", default="") 9 | 10 | @classmethod 11 | def poll(cls, context): 12 | return len(dir(bpy.ops.logic)) \ 13 | and bpy.ops.logic.python_component_register.poll() \ 14 | and bpy.ops.object.game_property_new.poll() 15 | 16 | def execute(self, context): 17 | bpy.ops.logic.python_component_register(component_name=self.component) 18 | if self.component != 'audvis.bge.Updater': 19 | bpy.ops.object.game_property_new(type='FLOAT', name="audvis_value") 20 | context.object.game.components[-1].properties['property_name'].value = context.object.game.properties[ 21 | -1].name 22 | return {"FINISHED"} 23 | -------------------------------------------------------------------------------- /ui/buttonspanel.py: -------------------------------------------------------------------------------- 1 | from bpy.types import Panel 2 | 3 | 4 | class AudVisButtonsPanel_Npanel(Panel): 5 | bl_space_type = 'VIEW_3D' 6 | bl_region_type = 'UI' 7 | bl_category = "AudVis" 8 | bl_options = {'DEFAULT_CLOSED'} 9 | 10 | @classmethod 11 | def poll(cls, context): 12 | pass 13 | 14 | 15 | class SequencerButtonsPanel_Update: 16 | """Use for subpanels""" 17 | pass 18 | -------------------------------------------------------------------------------- /ui/daw_arrangement/.gitignore: -------------------------------------------------------------------------------- 1 | /*.blend1 2 | -------------------------------------------------------------------------------- /ui/daw_arrangement/DawArrangement-blender3_6.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/example-sk/audvis/35e7aa673bd9d57cfe9175937fca273fb4c194f7/ui/daw_arrangement/DawArrangement-blender3_6.blend -------------------------------------------------------------------------------- /ui/daw_arrangement/parser/ableton_color_map.py: -------------------------------------------------------------------------------- 1 | ableton_color_map = { 2 | "0": "#FF8CA2", 3 | "1": "#FDAD33", 4 | "2": "#C9A234", 5 | "3": "#F1FF8A", 6 | "4": "#AFFF50", 7 | "5": "#00FF62", 8 | "6": "#00FFB4", 9 | "7": "#4AFFEC", 10 | "8": "#92BAFC", 11 | "9": "#6664DF", 12 | "10": "#9D91FB", 13 | "11": "#E132DC", 14 | "12": "#EA2999", 15 | "13": "#FFFFFF", 16 | "14": "#FF161F", 17 | "15": "#F76C00", 18 | "16": "#98764D", 19 | "17": "#F7FF56", 20 | "18": "#6DFF81", 21 | "19": "#00D240", 22 | "20": "#00C4B3", 23 | "21": "#0FE9FF", 24 | "22": "#3695EB", 25 | "23": "#296EBD", 26 | "24": "#943BDE", 27 | "25": "#BD5FC0", 28 | "26": "#FF00CA", 29 | "27": "#D0D0D0", 30 | "28": "#E46255", 31 | "29": "#FFA573", 32 | "30": "#D1B375", 33 | "31": "#E6FFB6", 34 | "32": "#CCEE9F", 35 | "33": "#B3DB7E", 36 | "34": "#94CD93", 37 | "35": "#D0FFE4", 38 | "36": "#CDF1F8", 39 | "37": "#BDBBE1", 40 | "38": "#D1B3E1", 41 | "39": "#B585E0", 42 | "40": "#E5DAE1", 43 | "41": "#A9A9A9", 44 | "42": "#C7918A", 45 | "43": "#B68558", 46 | "44": "#97866B", 47 | "45": "#BAC372", 48 | "46": "#9DCC39", 49 | "47": "#72BC5B", 50 | "48": "#85C4BC", 51 | "49": "#9DB0C3", 52 | "50": "#87A0C0", 53 | "51": "#8988C8", 54 | "52": "#A88FB3", 55 | "53": "#C298BB", 56 | "54": "#C06692", 57 | "55": "#7B7B7B", 58 | "56": "#B0292A", 59 | "57": "#AA512E", 60 | "58": "#724F40", 61 | "59": "#D5D035", 62 | "60": "#7EA136", 63 | "61": "#42AB45", 64 | "62": "#00A191", 65 | "63": "#295E83", 66 | "64": "#330091", 67 | "65": "#3F389E", 68 | "66": "#6C1DA8", 69 | "67": "#AA0EA7", 70 | "68": "#D00066", 71 | "69": "# 3C3C3C", 72 | 73 | } 74 | -------------------------------------------------------------------------------- /ui/daw_arrangement/parser/audiofile.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | import aud 3 | 4 | from ..arrangement import (Arrangement, Track, Clip, Note, TempoEvent, Audio) 5 | from ...props.daw_arrangement import AudvisDawArrangement 6 | 7 | 8 | def parse(filepath, props) -> Arrangement: 9 | return AudioFileParser(filepath, props).arrangement 10 | 11 | 12 | class AudioFileParser: 13 | arrangement: Arrangement 14 | 15 | def __init__(self, filepath, props: AudvisDawArrangement): 16 | if not os.path.exists(filepath): 17 | return 18 | self.arrangement = Arrangement() 19 | self.arrangement.basic_bpm = 110 # dummy value 20 | self.arrangement.load_audio(filepath, "tmp", props.audio_internal_samplerate) 21 | sound = aud.Sound(filepath) 22 | duration = sound.length / sound.specs[0] 23 | duration = duration / 60 * self.arrangement.basic_bpm 24 | track = Track(name=os.path.basename(filepath), color=(1, 1, 1, 1)) 25 | clip = Clip(time=0, duration=duration, name="", color=(1, 1, 1, 1,)) 26 | clip.audio.append( 27 | Audio(time=0, duration=clip.duration, 28 | play_start=0, loop_start=0, loop_end=clip.duration, 29 | warps=[(0, 0), (clip.duration, sound.length / sound.specs[0])], 30 | raw_data=self.arrangement.audio_map["tmp"])) 31 | track.clips.append(clip) 32 | self.arrangement.tracks.append(track) 33 | self.arrangement.calc_duration() 34 | -------------------------------------------------------------------------------- /ui/daw_arrangement/parser/midi.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from ..arrangement import (Arrangement, Track, Clip, Note, TempoEvent) 4 | from ...props.daw_arrangement import AudvisDawArrangement 5 | 6 | 7 | def parse(filename, props): 8 | try: 9 | import mido 10 | except: 11 | return 12 | 13 | class MidiParser: 14 | def __init__(self, filename: str, props: AudvisDawArrangement): 15 | midi = mido.MidiFile(filename) 16 | self.ticks_per_beat = midi.ticks_per_beat 17 | self.arrangement = Arrangement() 18 | midi_track: mido.MidiTrack 19 | for midi_track in midi.tracks: 20 | track = Track(name=midi_track.name, color=(1, 1, 1, 1)) 21 | self.parse_tempo(midi_track) 22 | (notes, duration) = self.parse_notes(midi_track) 23 | if len(notes): 24 | clip = Clip(time=0.0, color=(1, 1, 1, 1), duration=duration, name="") 25 | clip.notes = notes 26 | track.clips.append(clip) 27 | self.arrangement.tracks.append(track) 28 | self.arrangement.duration = max(self.arrangement.duration, clip.duration) 29 | for track in self.arrangement.tracks: 30 | track.clips[0].duration = self.arrangement.duration 31 | self.arrangement.print() 32 | 33 | def parse_tempo(self, midi_track): 34 | result = [] 35 | time = 0.0 36 | time_signature = (4, 4) 37 | for msg in midi_track: 38 | time += msg.time 39 | if msg.type == 'time_signature': 40 | time_signature = (msg.numerator, msg.denominator) 41 | if msg.type == 'set_tempo': 42 | if len(result) > 0: 43 | result.append( 44 | TempoEvent(tempo=result[-1].tempo, interpolation="constant", 45 | time=time / self.ticks_per_beat) 46 | ) 47 | result.append(TempoEvent( 48 | tempo=mido.tempo2bpm(msg.tempo, time_signature), 49 | interpolation="constant", 50 | time=time / self.ticks_per_beat 51 | )) 52 | if len(result) > 0: 53 | self.arrangement.basic_bpm = result[0].tempo 54 | if len(result) > 1: 55 | self.arrangement.tempo_changes = result 56 | 57 | def parse_notes(self, midi_track): 58 | started_notes = {} 59 | time = 0.0 60 | result = [] 61 | max_time = 0.0 62 | for msg in midi_track: 63 | time += msg.time 64 | if msg.type == 'note_on': 65 | note = Note(time=time / self.ticks_per_beat, velocity=msg.velocity, ch=1, duration=0, key=msg.note, 66 | rel=0.0) 67 | started_notes[msg.note] = note 68 | result.append(note) 69 | elif msg.type == 'note_off': 70 | if msg.note in started_notes: 71 | started_notes[msg.note].duration = time / self.ticks_per_beat - started_notes[msg.note].time 72 | del started_notes[msg.note] 73 | max_time = time / self.ticks_per_beat 74 | return (result, max_time) 75 | 76 | return MidiParser(filename, props).arrangement 77 | -------------------------------------------------------------------------------- /ui/force_reload.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import bpy 4 | 5 | 6 | class AUDVIS_OT_ForceReload(bpy.types.Operator): 7 | bl_idname = "audvis.forcereload" 8 | bl_label = "AudVis Force Reload" 9 | 10 | def execute(self, context): 11 | # this looks horrible, but it doesn't work if executed just once 12 | bpy.ops.preferences.addon_refresh() 13 | sys.modules[bpy.audvis._module_name].unregister() 14 | sys.modules[bpy.audvis._module_name].register() 15 | bpy.ops.preferences.addon_refresh() 16 | sys.modules[bpy.audvis._module_name].unregister() 17 | sys.modules[bpy.audvis._module_name].register() 18 | bpy.ops.preferences.addon_refresh() 19 | return {"CANCELLED"} # FINISHED causes crash 20 | 21 | 22 | classes = [AUDVIS_OT_ForceReload, ] 23 | -------------------------------------------------------------------------------- /ui/generator/material.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | 4 | def generate_material(audvis_config, freq_from, freq_to): 5 | if audvis_config.example_material == 'None': 6 | return None 7 | 8 | if audvis_config.example_material == 'Copy+Modify': 9 | if audvis_config.example_material_material is not None: 10 | return _copy_modify(audvis_config, freq_from, freq_to) 11 | return 12 | 13 | mat = bpy.data.materials.new("AudVisExample") 14 | if hasattr(bpy.data, "collections"): # blender 2.80 15 | mat.diffuse_color = audvis_config.example_material_basecolor 16 | else: 17 | col = audvis_config.example_material_basecolor 18 | mat.diffuse_color = (col[0], col[1], col[2]) 19 | if audvis_config.example_material_channel == 'gray': 20 | channels = [0, 1, 2] 21 | else: 22 | channels = [int(audvis_config.example_material_channel)] 23 | expr = "audvis(%s,%s) / 10" % (freq_from, freq_to) 24 | _driver_channel(mat, channels, expr) 25 | return mat 26 | 27 | 28 | def _copy_modify(audvis_config, freq_from, freq_to): 29 | material = audvis_config.example_material_material.copy() 30 | if material.node_tree is not None and material.node_tree.animation_data is not None: 31 | for driver in material.node_tree.animation_data.drivers: 32 | exp = driver.driver.expression 33 | driver.driver.expression = exp.replace("audvis()", "audvis(%s,%s)" % (freq_from, freq_to)) 34 | return material 35 | 36 | 37 | def _driver_channel(mat, channels, expr): 38 | for ch in channels: 39 | d = mat.driver_add("diffuse_color", int(ch)) 40 | d.driver.expression = expr 41 | -------------------------------------------------------------------------------- /ui/global_settings.py: -------------------------------------------------------------------------------- 1 | # if Scene settings is not enough global 2 | 3 | import bpy 4 | 5 | 6 | class GlobalSettings: 7 | setting_name = None 8 | default = 0 9 | 10 | def __init__(self, setting_name, default=0): 11 | self.setting_name = setting_name 12 | self.default = default 13 | 14 | def _get_obj(self, create=False): 15 | name = 'audvis_global_settings' 16 | if name in bpy.data.objects: 17 | obj = bpy.data.objects[name] 18 | else: 19 | obj = bpy.data.objects.new(name, None) 20 | if create: 21 | obj.use_fake_user = True 22 | return obj 23 | 24 | def get(self, audvis_settings): 25 | obj = self._get_obj() 26 | if self.setting_name in obj: 27 | return obj[self.setting_name] 28 | return 0 29 | 30 | def set(self, audvis_settings, value): 31 | self._get_obj(create=True)[self.setting_name] = value 32 | 33 | def getter(self): # override "TypeError: get keyword: expected a function type, not a method" 34 | def anon(audvis_settings): 35 | return self.get(audvis_settings) 36 | 37 | return anon 38 | 39 | def setter(self): # override "TypeError: set keyword: expected a function type, not a method" 40 | def anon(audvis_settings, value): 41 | return self.set(audvis_settings, value) 42 | 43 | return anon 44 | -------------------------------------------------------------------------------- /ui/hz_label.py: -------------------------------------------------------------------------------- 1 | from ..note_calculator import calculate_note 2 | 3 | 4 | def hz_label(start, range_per_point, step, points_count): 5 | return "{:,.2f} - {:,.2f} Hz ".format( 6 | start, 7 | start + (points_count - 1) * step + range_per_point 8 | ) 9 | 10 | 11 | # https://pages.mtu.edu/~suits/NoteFreqCalcs.html 12 | def notes_label(count=1, step=1, a4=440, note_steps_offset=50): 13 | start_freq = calculate_note(note_steps_offset * step, a4) 14 | end_freq = calculate_note((count + note_steps_offset) * step, a4) 15 | # print([count, start_freq, end_freq]) 16 | return "{:,.2f} - {:,.2f} Hz".format( 17 | start_freq, 18 | end_freq 19 | ) 20 | -------------------------------------------------------------------------------- /ui/install_ui/__init__.py: -------------------------------------------------------------------------------- 1 | from . import ( 2 | all, 3 | realtime, 4 | recording, 5 | video, 6 | ) 7 | 8 | classes = [ 9 | all.AUDVIS_OT_InstallAll, 10 | realtime.AUDVIS_OT_RealtimeUninstall, 11 | recording.AUDVIS_OT_RealtimeUninstallSoundRecorder, 12 | video.AUDVIS_OT_VideoUninstall, 13 | ] 14 | -------------------------------------------------------------------------------- /ui/install_ui/all.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | from .. import install_lib 4 | 5 | 6 | class AUDVIS_OT_InstallAll(bpy.types.Operator): 7 | bl_idname = "audvis.install" 8 | bl_label = "Install all libraries for AudVis" 9 | bl_description = "Install needed python modules" 10 | 11 | @classmethod 12 | def poll(cls, context): 13 | return True 14 | # return not bpy._audvis_module.startswith("bl_ext.") 15 | 16 | def execute(self, context): 17 | install_lib.PipInstaller().install() 18 | return {'FINISHED'} 19 | -------------------------------------------------------------------------------- /ui/install_ui/realtime.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | from .. import install_lib 4 | 5 | 6 | class AUDVIS_OT_RealtimeUninstall(bpy.types.Operator): 7 | bl_idname = "audvis.realtime_uninstall" 8 | bl_label = "Uninstall audvis realtime support" 9 | bl_description = "Install needed python modules" 10 | 11 | def execute(self, context): 12 | install_lib.PipInstaller().uninstall("sd") 13 | return {'FINISHED'} 14 | -------------------------------------------------------------------------------- /ui/install_ui/recording.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | from .. import install_lib 4 | 5 | 6 | class AUDVIS_OT_RealtimeUninstallSoundRecorder(bpy.types.Operator): 7 | bl_idname = "audvis.realtime_uninstall_soundrecorder" 8 | bl_label = "Uninstall audvis sound recording support" 9 | bl_description = "Uninstall needed python modules" 10 | 11 | def execute(self, context): 12 | install_lib.PipInstaller().uninstall("recorder") 13 | return {'FINISHED'} 14 | -------------------------------------------------------------------------------- /ui/install_ui/video.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | from .. import install_lib 4 | 5 | 6 | class AUDVIS_OT_VideoUninstall(bpy.types.Operator): 7 | bl_idname = "audvis.video_uninstall" 8 | bl_label = "Uninstall audvis video support" 9 | bl_description = "Uninstall needed python modules" 10 | 11 | def execute(self, context): 12 | install_lib.PipInstaller().uninstall("cv2") 13 | return {'FINISHED'} 14 | -------------------------------------------------------------------------------- /ui/midi/__init__.py: -------------------------------------------------------------------------------- 1 | from . import realtime, file 2 | from .realtime import input_device_options 3 | 4 | classes = file.classes + realtime.classes 5 | -------------------------------------------------------------------------------- /ui/midi/file.py: -------------------------------------------------------------------------------- 1 | from bpy.types import (Operator, UIList) 2 | from .operators import ( 3 | midiFileRemove, 4 | midiFileOpen, 5 | midiTrackRemove, 6 | ) 7 | import bpy 8 | import re 9 | 10 | from ..buttonspanel import (AudVisButtonsPanel_Npanel) 11 | from .utils import (get_selected_midi_file, get_selected_midi_track) 12 | 13 | 14 | class AUDVIS_UL_midiTrackList(UIList): 15 | def filter_items(self, context, data, propname): 16 | items = getattr(data, propname) 17 | filtered = [self.bitflag_filter_item] * len(items) 18 | ordered = [index for index, item in enumerate(items)] 19 | for i in range(len(items)): 20 | if items[i].deleted: 21 | filtered[i] &= ~self.bitflag_filter_item 22 | return filtered, ordered 23 | 24 | def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): 25 | if self.layout_type in {'DEFAULT', 'COMPACT'}: 26 | layout.prop(item, "enable", text="") 27 | layout.label(text=item.name) 28 | elif self.layout_type in {'GRID'}: 29 | layout.alignment = 'CENTER' 30 | layout.label(text="") 31 | 32 | 33 | class AUDVIS_UL_midiFileList(UIList): 34 | def filter_items(self, context, data, propname): 35 | items = getattr(data, propname) 36 | filtered = [self.bitflag_filter_item] * len(items) 37 | ordered = [index for index, item in enumerate(items)] 38 | for i in range(len(items)): 39 | if items[i].deleted: 40 | filtered[i] &= ~self.bitflag_filter_item 41 | return filtered, ordered 42 | 43 | def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): 44 | custom_icon = 'SOUND' 45 | 46 | # Make sure your code supports all 3 layout types 47 | if self.layout_type in {'DEFAULT', 'COMPACT'}: 48 | layout.prop(item, "enable", text="") 49 | layout.label(text=item.name) 50 | elif self.layout_type in {'GRID'}: 51 | layout.alignment = 'CENTER' 52 | layout.label(text=item.name, icon=custom_icon) 53 | 54 | 55 | regexp = re.compile('ch([0-9]+)_(n|c)([0-9]+)') 56 | 57 | 58 | class AUDVIS_PT_midiFileNpanel(AudVisButtonsPanel_Npanel): 59 | bl_label = "Midi File" 60 | 61 | @classmethod 62 | def poll(cls, context): 63 | return True 64 | 65 | def draw_header(self, context): 66 | self.layout.prop(context.scene.audvis.midi_file, "enable", text="") 67 | 68 | def draw(self, context): 69 | layout = self.layout 70 | props = context.scene.audvis.midi_file 71 | col = layout.column(align=True) 72 | supported = bpy.audvis.is_midi_realtime_supported() 73 | if not supported: 74 | col.label(text="Midi files not supported. Install mido first:") 75 | col.operator("audvis.install", text="Install python packages") 76 | return 77 | col.template_list("AUDVIS_UL_midiFileList", "midi_file_list", 78 | context.scene.audvis.midi_file, "midi_files", 79 | props, "list_index") 80 | row = col.row() 81 | row.operator('audvis.midi_file_open') 82 | row.operator('audvis.midi_file_remove') 83 | midifile = get_selected_midi_file(context) 84 | if midifile is not None: 85 | col = layout.column(align=True) 86 | col.prop(midifile, "name") 87 | col.prop(midifile, "frame_start") 88 | col.prop(midifile, "animation_offset_start") 89 | col.prop(midifile, "animation_offset_end") 90 | col.label(text="Tracks for {}:".format(midifile.name)) 91 | col.template_list("AUDVIS_UL_midiTrackList", "midi_track_list", 92 | midifile, "tracks", 93 | midifile, "list_index") 94 | self._draw_track_info(col, context) 95 | 96 | def _draw_track_info(self, col, context): 97 | track = get_selected_midi_track(context) 98 | midifile = get_selected_midi_file(context) 99 | if track is None: 100 | return 101 | col.prop(track, "name") 102 | col.operator('audvis.midi_track_remove') 103 | # col = layout.column(align=True) 104 | for key in track.keys(): 105 | regexp_result = regexp.match(key) 106 | if regexp_result: 107 | channel = regexp_result.group(1) 108 | type = regexp_result.group(2) 109 | note = regexp_result.group(3) 110 | col.label(text="Driver expression (example):") 111 | expr = "audvis({}={}, ch={}, track={}, file={})".format( 112 | "midi" if type == 'n' else "midi_control", 113 | int(note), 114 | int(channel), 115 | repr(track.name), 116 | repr(midifile.name) 117 | ) 118 | col.label(text=expr) 119 | op = col.operator("audvis.copy_string", text="Copy Driver Expression to Clipboard") 120 | op.value = expr 121 | break 122 | 123 | 124 | classes = [ 125 | AUDVIS_UL_midiFileList, 126 | AUDVIS_UL_midiTrackList, 127 | AUDVIS_PT_midiFileNpanel, 128 | midiFileOpen.AUDVIS_OT_midiFileOpen, 129 | midiFileRemove.AUDVIS_OT_midiFileRemove, 130 | midiTrackRemove.AUDVIS_OT_midiTrackRemove, 131 | ] 132 | -------------------------------------------------------------------------------- /ui/midi/operators/midiFileOpen.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import sys 3 | from bpy.types import ( 4 | Operator, 5 | ) 6 | import bpy 7 | from bpy_extras.io_utils import ImportHelper 8 | 9 | from .. import midi_file_baker 10 | 11 | 12 | class AUDVIS_OT_midiFileOpen(Operator, ImportHelper): 13 | bl_idname = "audvis.midi_file_open" 14 | bl_label = "Add Midi File" 15 | bl_description = "" 16 | 17 | filter_glob: bpy.props.StringProperty( 18 | default='*.mid;*.midi', 19 | options={'HIDDEN'} 20 | ) 21 | 22 | strip_silent_start: bpy.props.BoolProperty( 23 | name="Strip Silent Beginning of MIDI", 24 | description="MIDI files use to have sometimes quite a long time from beginning to first note." 25 | " This cuts that empty, silent part.", 26 | default=True 27 | ) 28 | 29 | @classmethod 30 | def poll(cls, context): 31 | return bpy.audvis.is_midi_realtime_supported() 32 | 33 | def execute(self, context): 34 | midi_file_baker.bake(context.scene, self.filepath, self.strip_silent_start) 35 | 36 | return {'FINISHED'} 37 | -------------------------------------------------------------------------------- /ui/midi/operators/midiFileRemove.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from bpy.types import ( 4 | Operator, 5 | ) 6 | import bpy 7 | 8 | from ..utils import get_selected_midi_file 9 | 10 | 11 | class AUDVIS_OT_midiFileRemove(Operator): 12 | bl_idname = "audvis.midi_file_remove" 13 | bl_label = "Remove Midi File" 14 | bl_description = "" 15 | 16 | @classmethod 17 | def poll(cls, context): 18 | if not bpy.audvis.is_midi_realtime_supported(): 19 | return False 20 | if get_selected_midi_file(context) is None: 21 | return False 22 | return True 23 | 24 | def execute(self, context): 25 | midifile = get_selected_midi_file(context) 26 | if midifile is not None \ 27 | and context.scene.animation_data \ 28 | and context.scene.animation_data.action is not None: 29 | fcurves = context.scene.animation_data.action.fcurves 30 | for fcurve in fcurves: 31 | if fcurve.data_path.startswith(midifile.path_from_id()): 32 | fcurves.remove(fcurve) 33 | midifile.deleted = True 34 | midifile.enable = False 35 | midifile.name = '_deleted' 36 | midifile.filepath = '' 37 | midifile.tracks.clear() 38 | # TODO: try to really delete? Check if fcurves work how expected 39 | return {'FINISHED'} 40 | -------------------------------------------------------------------------------- /ui/midi/operators/midiTrackRemove.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from bpy.types import ( 4 | Operator, 5 | ) 6 | import bpy 7 | 8 | from ..utils import get_selected_midi_track 9 | 10 | 11 | class AUDVIS_OT_midiTrackRemove(Operator): 12 | bl_idname = "audvis.midi_track_remove" 13 | bl_label = "Remove Midi Track" 14 | bl_description = "" 15 | 16 | @classmethod 17 | def poll(cls, context): 18 | if not bpy.audvis.is_midi_realtime_supported(): 19 | return False 20 | if get_selected_midi_track(context) is None: 21 | return False 22 | return True 23 | 24 | def execute(self, context): 25 | track = get_selected_midi_track(context) 26 | if track is not None \ 27 | and context.scene.animation_data \ 28 | and context.scene.animation_data.action is not None: 29 | fcurves = context.scene.animation_data.action.fcurves 30 | for fcurve in fcurves: 31 | if fcurve.data_path.startswith(track.path_from_id()): 32 | fcurves.remove(fcurve) 33 | track.deleted = True 34 | track.enable = False 35 | track.name = '_deleted' 36 | # TODO: try to really delete? Check if fcurves work how expected 37 | return {'FINISHED'} 38 | -------------------------------------------------------------------------------- /ui/midi/utils.py: -------------------------------------------------------------------------------- 1 | def get_selected_midi_file(context): 2 | props = context.scene.audvis.midi_file 3 | if 0 <= props.list_index < len(props.midi_files): 4 | midifile = props.midi_files[props.list_index] 5 | if midifile.deleted: 6 | return None 7 | return midifile 8 | return None 9 | 10 | 11 | def get_selected_midi_track(context): 12 | props = context.scene.audvis.midi_file 13 | midifile = get_selected_midi_file(context) 14 | if midifile is None: 15 | return None 16 | if 0 <= midifile.list_index < len(midifile.tracks): 17 | track = midifile.tracks[midifile.list_index] 18 | if track.deleted: 19 | return None 20 | return track 21 | return None -------------------------------------------------------------------------------- /ui/partymode/__init__.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | from . import gizmo 4 | from . import (in_window, in_workspace) 5 | from . import party_panel 6 | 7 | 8 | class AUDVIS_OT_Partymode(bpy.types.Operator): 9 | bl_idname = "audvis.partymode" 10 | bl_label = "Enter Party Mode" 11 | bl_description = "Opens full screen 3d area.\nTo exit party mode, press Alt+F4" 12 | party_implementation: bpy.props.StringProperty(name="Party Mode Implementation") 13 | override = None 14 | 15 | def invoke(self, context, event): 16 | self.party_implementation = context.scene.audvis.party.implementation 17 | context.window_manager.modal_handler_add(self) 18 | if self.party_implementation == 'workspace': 19 | return in_workspace.invoke(self, context, event) 20 | elif self.party_implementation == 'window': 21 | return in_window.invoke(self, context, event) 22 | 23 | def modal(self, context, event): 24 | if self.party_implementation == 'workspace': 25 | return in_workspace.modal(self, context, event) 26 | elif self.party_implementation == 'window': 27 | return in_window.modal(self, context, event) 28 | 29 | 30 | def render_menuitem(self, context): 31 | self.layout.separator() 32 | self.layout.operator("audvis.partymode", text="Enter Party Mode") 33 | 34 | 35 | def register(): 36 | try: 37 | bpy.types.TOPBAR_MT_render.append(render_menuitem) 38 | except: 39 | pass 40 | 41 | 42 | def unregister(): 43 | try: 44 | bpy.types.TOPBAR_MT_render.remove(render_menuitem) 45 | except: 46 | pass 47 | 48 | 49 | classes = gizmo.classes + [ 50 | AUDVIS_OT_Partymode, 51 | ] + party_panel.classes 52 | -------------------------------------------------------------------------------- /ui/partymode/audvis-party-workspace.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/example-sk/audvis/35e7aa673bd9d57cfe9175937fca273fb4c194f7/ui/partymode/audvis-party-workspace.blend -------------------------------------------------------------------------------- /ui/partymode/gizmo.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | from ...utils import call_ops_override 4 | 5 | partymode_windows = [] # injected from open window operator 6 | 7 | from bpy.types import ( 8 | GizmoGroup, 9 | ) 10 | 11 | 12 | def is_party_mode(context): 13 | # return True 14 | if context.scene.audvis.party.implementation == 'workspace': 15 | return context.workspace.name.startswith('audvis-party') 16 | return context.window in partymode_windows 17 | 18 | 19 | class AUDVIS_OT_PartymodeClose(bpy.types.Operator): 20 | bl_idname = "audvis.partymodeclose" 21 | bl_label = "Close the Party Mode Window" 22 | bl_description = bl_label 23 | 24 | @classmethod 25 | def poll(self, context): 26 | return is_party_mode(context) 27 | 28 | def invoke(self, context, event): 29 | if context.scene.audvis.party.implementation == 'workspace': 30 | if context.workspace.name.startswith('audvis-party'): 31 | self._close_workspace(context) 32 | else: 33 | if context.window in partymode_windows: 34 | partymode_windows.remove(context.window) 35 | bpy.ops.wm.window_close() 36 | return {'FINISHED'} 37 | 38 | def _close_workspace(self, context): 39 | if bpy.context.screen.is_animation_playing: 40 | bpy.ops.screen.animation_play() 41 | bpy.ops.wm.window_fullscreen_toggle() 42 | bpy.ops.screen.screen_full_area(use_hide_panels=True) 43 | workspace = context.workspace 44 | bpy.ops.screen.workspace_cycle('INVOKE_DEFAULT') 45 | call_ops_override(bpy.ops.workspace.delete, {'workspace': workspace}) 46 | 47 | 48 | class AUDVIS_partymode_gizmogroup(GizmoGroup): 49 | bl_idname = "AUDVIS_partymode_gizmogroup" 50 | bl_label = "AudVis Party Mode Gizmo Group" 51 | bl_space_type = 'VIEW_3D' 52 | bl_region_type = 'WINDOW' 53 | bl_options = {'PERSISTENT', 'SCALE'} 54 | 55 | @classmethod 56 | def setup_keymap(cls, keyconfig): 57 | keymap = bpy.context.window_manager.keyconfigs.addon.keymaps.new('audvis_partymode_gizmo') 58 | keymap.keymap_items.new('audvis.partymodeclose', 'LEFTMOUSE', 'CLICK') 59 | return keymap 60 | 61 | @classmethod 62 | def poll(cls, context): 63 | is_party = is_party_mode(context) 64 | return not bpy.context.screen.is_animation_playing and is_party 65 | 66 | def draw_prepare(self, context): 67 | region = context.region 68 | self.close_gizmo.matrix_basis[0][3] = region.width - 40 # left 69 | self.close_gizmo.matrix_basis[1][3] = 40 # bottom 70 | 71 | def setup(self, context): 72 | mpr = self.gizmos.new("GIZMO_GT_button_2d") # This line crashes Blender 73 | mpr.icon = 'PANEL_CLOSE' 74 | mpr.draw_options = {'BACKDROP', 'OUTLINE'} 75 | mpr.target_set_operator('audvis.partymodeclose') 76 | 77 | mpr.alpha = 0.2 78 | mpr.color_highlight = 0.8, 0.8, 0.8 79 | mpr.alpha_highlight = .8 80 | 81 | mpr.scale_basis = (80 * 0.35) / 2 # Same as buttons defined in C 82 | self.close_gizmo = mpr 83 | 84 | 85 | classes = [ 86 | AUDVIS_OT_PartymodeClose, 87 | AUDVIS_partymode_gizmogroup, 88 | ] 89 | -------------------------------------------------------------------------------- /ui/partymode/in_window.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | from ...utils import call_ops_override 4 | from . import gizmo 5 | 6 | 7 | def _hide_cursor(context, window): 8 | if not context.scene.audvis.party.hide_mouse: 9 | return 10 | 11 | def _tmp(): 12 | window.cursor_set("NONE") 13 | 14 | bpy.app.timers.register(_tmp, first_interval=5) 15 | 16 | 17 | def invoke(operator, context, event): 18 | operator.callbacks = [] 19 | if True: 20 | bpy.ops.wm.window_new() 21 | else: 22 | bpy.ops.screen.userpref_show('INVOKE_DEFAULT') 23 | window = context.window_manager.windows[-1] 24 | gizmo.partymode_windows.append(window) 25 | area = window.screen.areas[0] 26 | area.type = 'VIEW_3D' 27 | space = area.spaces[0] 28 | space.overlay.show_overlays = False 29 | space.shading.type = context.scene.audvis.party.shading 30 | space.shading.show_xray_wireframe = context.scene.audvis.party.show_xray 31 | space.region_3d.view_perspective = 'CAMERA' 32 | call_ops_override(bpy.ops.screen.screen_full_area, { 33 | 'window': window, 34 | 'screen': window.screen, 35 | 'area': area, 36 | }, use_hide_panels=True) 37 | space.show_region_header = False 38 | call_ops_override(bpy.ops.wm.window_fullscreen_toggle, {'window': window}) 39 | bpy.ops.object.select_all(action='DESELECT') 40 | _hide_cursor(context, window) 41 | if not window.screen.is_animation_playing: 42 | bpy.ops.screen.animation_play() 43 | screen = window.screen 44 | area = screen.areas[0] 45 | space = area.spaces[0] 46 | if hasattr(space, 'show_gizmo'): 47 | space.show_gizmo = True 48 | space.show_gizmo_navigate = False 49 | context.window_manager.windows.update() 50 | 51 | def _zoom(): 52 | call_ops_override(bpy.ops.view3d.view_center_camera, { 53 | 'window': window, 54 | 'screen': screen, 55 | 'area': area, 56 | 'region': area.regions[0], 57 | }) 58 | return None 59 | 60 | bpy.app.timers.register(_zoom, first_interval=1) 61 | return {'RUNNING_MODAL'} 62 | 63 | 64 | def modal(operator, context, event): 65 | return {'FINISHED'} 66 | -------------------------------------------------------------------------------- /ui/partymode/in_workspace.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import bpy 4 | 5 | from ...utils import call_ops_override 6 | 7 | workspace_name = 'audvis-party' 8 | 9 | 10 | def _hide_cursor(context, window): 11 | if not context.scene.audvis.party.hide_mouse: 12 | return 13 | 14 | def _tmp(): 15 | window.cursor_set("NONE") 16 | 17 | bpy.app.timers.register(_tmp, first_interval=5) 18 | 19 | 20 | def invoke(operator, context, event): 21 | operator.callbacks = [] 22 | blend_path = os.path.join(os.path.dirname(__file__), 'audvis-party-workspace.blend') 23 | workspace_names = bpy.data.workspaces.keys() 24 | bpy.ops.workspace.append_activate(idname="audvis-party", filepath=blend_path) 25 | new_workspace_names = bpy.data.workspaces.keys() 26 | found_new_ws_names = [x for x in new_workspace_names if x not in workspace_names] 27 | operator._workspace = bpy.data.workspaces[found_new_ws_names[0]] 28 | # operator._workspace = bpy.context.workspace 29 | bpy.ops.wm.window_fullscreen_toggle() 30 | bpy.context.window_manager.windows.update() 31 | bpy.ops.object.select_all(action='DESELECT') 32 | operator.callbacks.append(lambda: _play()) 33 | operator.callbacks.append(lambda: _fullscreen(operator)) 34 | window = context.window 35 | _hide_cursor(context, window) 36 | operator.timer = context.window_manager.event_timer_add(.01, window=window) 37 | return {'RUNNING_MODAL'} 38 | 39 | 40 | def _play(requested_state=True): 41 | if bpy.context.screen.is_animation_playing != requested_state: 42 | bpy.ops.screen.animation_play() 43 | 44 | 45 | def _fullscreen(operator): 46 | workspace = operator._workspace 47 | override = { 48 | 'screen': workspace.screens[0], 49 | 'area': workspace.screens[0].areas[0] 50 | } 51 | call_ops_override(bpy.ops.screen.screen_full_area, override, use_hide_panels=True) 52 | space = bpy.context.space_data 53 | if space is None: 54 | print("Known bug: can't setup vie3d options in the party workspace mode") 55 | else: 56 | space.overlay.show_overlays = False 57 | space.shading.type = bpy.context.scene.audvis.party.shading 58 | space.shading.show_xray_wireframe = bpy.context.scene.audvis.party.show_xray 59 | space.region_3d.view_perspective = 'CAMERA' 60 | 61 | 62 | def modal(operator, context, event): 63 | if event.type == 'TIMER': 64 | if len(operator.callbacks): 65 | cb = operator.callbacks.pop(0) 66 | cb() 67 | else: 68 | context.window_manager.event_timer_remove(operator.timer) 69 | return {'FINISHED'} 70 | else: 71 | return {'PASS_THROUGH'} 72 | return {'RUNNING_MODAL'} 73 | -------------------------------------------------------------------------------- /ui/partymode/party_panel.py: -------------------------------------------------------------------------------- 1 | from ..buttonspanel import (AudVisButtonsPanel_Npanel) 2 | 3 | 4 | class AUDVIS_PT_partymodeNpanel(AudVisButtonsPanel_Npanel): 5 | bl_label = "Party Mode" 6 | 7 | @classmethod 8 | def poll(cls, context): 9 | return True 10 | 11 | def draw(self, context): 12 | props = context.scene.audvis.party 13 | col = self.layout.column(align=True) 14 | col.prop(props, "implementation") 15 | col.prop(props, "shading") 16 | col.prop(props, "hide_mouse") 17 | if props.shading == 'WIREFRAME': 18 | col.prop(props, "show_xray") 19 | col.operator("audvis.partymode", text="Enter Party Mode", icon='FULLSCREEN_ENTER') 20 | col.label(text="Note: Press ESC to end the Party Mode") 21 | if props.shading != 'RENDERED': 22 | col = self.layout.column(align=True) 23 | col.label(text="For more settings see") 24 | col.label(text="Blender Preferences -> Themes") 25 | col.prop(context.preferences.themes[0].view_3d.space.gradients, "high_gradient") 26 | col.operator("preferences.reset_default_theme") 27 | 28 | 29 | classes = [ 30 | AUDVIS_PT_partymodeNpanel, 31 | ] 32 | -------------------------------------------------------------------------------- /ui/pip_installer.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | import sys 4 | import threading 5 | from glob import glob 6 | 7 | 8 | class PipinstallerThread(threading.Thread): 9 | packages = [] 10 | status = 'init' 11 | 12 | # PipinstallerThread 13 | def add(self, packages, deps=True): 14 | if type(packages) == str: 15 | packages = [packages] 16 | self.packages.append([packages, deps]) 17 | return self 18 | 19 | def run(self): 20 | self.status = 'trying ensurepip' 21 | python_path = _get_python_path() 22 | subprocess.call([python_path, '-m', 'ensurepip', '--altinstall']) 23 | for item in self.packages: 24 | packages = item[0] 25 | deps = item[1] 26 | self.status = 'trying ' + ' '.join(packages) 27 | deps_args = [] 28 | if not deps: 29 | deps_args = ['--no-deps'] 30 | output = subprocess.check_output( 31 | [python_path, '-m', 'pip', 'install', '--force-reinstall'] + deps_args + packages, 32 | universal_newlines=True 33 | ) 34 | self.status = 'success' 35 | 36 | 37 | def _get_python_path(): 38 | f = glob(os.path.join(os.path.realpath(sys.prefix), 'bin', 'python*')) 39 | return f[0] 40 | -------------------------------------------------------------------------------- /ui/props/__init__.py: -------------------------------------------------------------------------------- 1 | from . import ( 2 | spectrogram, 3 | animationnodes, 4 | valuesaud, 5 | spreaddrivers, 6 | scene, 7 | sequence, 8 | shapemodifier, 9 | armaturegenerator, 10 | party, 11 | obj, 12 | midi, 13 | daw_arrangement, realtimeprops, 14 | ) 15 | 16 | classes = [ 17 | animationnodes.AudvisAnimationnodesProperties, 18 | valuesaud.AudvisValuesAudProperties, 19 | party.AudvisPartyProperties, 20 | ] + midi.classes + daw_arrangement.classes + realtimeprops.classes + [ 21 | spectrogram.AudvisSpectrogramProperties, 22 | spectrogram.AudvisSpectrogramMetaProperties, 23 | spreaddrivers.AudvisSceneSpreaddriversProperties, 24 | scene.AudvisSceneProperties, # all prop groups in scene need to be above this line 25 | sequence.AudvisSequenceProperties, 26 | shapemodifier.AudvisObjectShapemodifierProperties, 27 | armaturegenerator.AudvisObjectArmatureGeneratorProperties, 28 | obj.AudvisObjectProperties, 29 | ] 30 | -------------------------------------------------------------------------------- /ui/props/animationnodes.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | 4 | class AudvisAnimationnodesProperties(bpy.types.PropertyGroup): 5 | type: bpy.props.EnumProperty(name="Type", description="", items=[ 6 | ("script", "Script", 7 | "Creates a Script node and an Invoke Subprogram node. Use for modifying lists and more complex use cases."), 8 | ("expression", "Expression", "Creates an Expression node. Use for simpler use cases.") 9 | ]) 10 | -------------------------------------------------------------------------------- /ui/props/armaturegenerator.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | from .midi import AudvisMidiGeneratorsProperties 4 | from .. import shapemodifier 5 | 6 | 7 | class AudvisObjectArmatureGeneratorProperties(bpy.types.PropertyGroup): 8 | keep_old_vgroups: bpy.props.BoolProperty(name="Keep Old Vertex Groups", default=False) 9 | 10 | channel: bpy.props.IntProperty(name="Sound Channel", default=1, min=1, soft_max=32) 11 | freq_seq_type: bpy.props.EnumProperty(name="Freq Sequencing", items=[ 12 | ("classic", "Linear", "0-50 ; 50-100 ; 100-150..."), 13 | ("notes", "Notes", "Music notes"), 14 | ("midi", "MIDI Notes", "MIDI notes from .mid files or realtime from MIDI devices"), 15 | ]) 16 | midi: bpy.props.PointerProperty(name="MIDI", type=AudvisMidiGeneratorsProperties) 17 | note_a4_freq: bpy.props.FloatProperty(name="A4 Note Frequency", default=440.0, soft_min=432.0, soft_max=446.0) 18 | note_step: bpy.props.FloatProperty(name="Note Step", default=1, step=50) 19 | note_offset: bpy.props.IntProperty(name="Note Steps Offset", default=0) 20 | sound_sequence: bpy.props.StringProperty(name="Sound Sequence") 21 | sequence_channel: bpy.props.IntProperty(name="Sequence Channel", default=0, min=0, 22 | description="Channel number in Video Sequence Editor") 23 | 24 | freqrange: bpy.props.FloatProperty(name="Frequency Range Per Point", default=50, min=.01) 25 | freqstart: bpy.props.FloatProperty(name="Frequency Start", default=0, min=0) 26 | freq_step_enable: bpy.props.BoolProperty(name="Set Custom Step", default=False) 27 | freq_step: bpy.props.FloatProperty(name="Frequency Step", default=5.0, min=0) 28 | freq_step_calc: bpy.props.FloatProperty(name="Frequency Step Calculated", get=shapemodifier.calc_freq_step) 29 | factor: bpy.props.FloatProperty(name="Factor", default=.1, precision=4) 30 | add: bpy.props.FloatProperty(name="Add", default=0, precision=4) 31 | inset: bpy.props.BoolProperty(name="Inset", default=False) 32 | inset_and_extrude: bpy.props.BoolProperty(name="Extrude After Inset", default=0) 33 | inset_size: bpy.props.FloatProperty(name="Inset Size", default=0.01) 34 | armature_object: bpy.props.PointerProperty(type=bpy.types.Object, name="Armature Object") 35 | -------------------------------------------------------------------------------- /ui/props/daw_arrangement.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | 4 | class AudvisDawArrangement(bpy.types.PropertyGroup): 5 | filepath: bpy.props.StringProperty(name="File Path", subtype="FILE_PATH", description="*.dawproject or *.als file") 6 | zoom: bpy.props.FloatProperty(name="Zoom X", default=.3) 7 | thickness_clip: bpy.props.FloatProperty(name="Clip Thickness", default=.1) 8 | thickness_note: bpy.props.FloatProperty(name="Note Thickness", default=.05) 9 | zero_length_note_threshold: bpy.props.FloatProperty(name="Zero Length Note Threshold", default=.001, min=.0, precision=3) 10 | zero_length_note_size: bpy.props.FloatProperty(name="Zero Length Note Duration", default=.0, min=.0) 11 | frame_start: bpy.props.IntProperty(name="Start Frame", default=1) 12 | line_height: bpy.props.FloatProperty(name="Line Height", default=.5, min=0) 13 | line_margin: bpy.props.FloatProperty(name="Line Margin", default=.1) 14 | center_tracks: bpy.props.BoolProperty(name="Center Tracks", default=False, description="If enabled, tracks will be centered on Y axis. Otherwise they go from zero") 15 | clip_padding: bpy.props.FloatProperty(name="Clip Padding", default=10, min=0.0, max=100, subtype="PERCENTAGE") 16 | track_name_position: bpy.props.EnumProperty(name="Track Name Z Position", default=2, items=[ 17 | ("0", "Under Clips", ""), 18 | ("1", "Between Clip and Notes", ""), 19 | ("2", "Above Notes", ""), 20 | ]) 21 | track_name_bevel_depth: bpy.props.FloatProperty(name="Track Name Bevel Depth", default=0.0) 22 | # animate_property: bpy.props.EnumProperty(name="Animate Property", items=[ 23 | # ("location-x", "Location X", ""), 24 | # ("geonodes-progress", 'Geometry Nodes Input "Progress"', ""), 25 | # ], default="location-x") 26 | output: bpy.props.EnumProperty(name="Output Object(s)", items=[ 27 | ("geonodes1", "Geometry Nodes 1", ""), 28 | # ("geonodes2", "Geometry Nodes - Curves", ""), 29 | ], default="geonodes1") 30 | replace_last_collection: bpy.props.BoolProperty(name="Replace Last Collection", default=True) 31 | last_collection: bpy.props.PointerProperty(name="Last Collection", type=bpy.types.Collection) 32 | audio_internal_samplerate: bpy.props.IntProperty(name="Audio Internal Samplerate", default=50, min=1, max=10000) 33 | audio_use_abs: bpy.props.BoolProperty(name="Audio: Use Absolute Values", default=True) 34 | audio_horizontal: bpy.props.BoolProperty(name="Horizontal SoundWave", default=True) 35 | audio_curve_samplerate: bpy.props.IntProperty(name="Audio Curve Points Samplerate", default=50, min=1, max=10000) 36 | audio_algorithm: bpy.props.EnumProperty(name="Audio Algorithm", items=[ 37 | ("raw", "Raw", ""), 38 | ("log", "Logarithm", ""), 39 | ]) 40 | audio_amplitude: bpy.props.FloatProperty(name="Audio Amplitude", default=.1, min=.001, max=100) 41 | 42 | 43 | classes = [ 44 | AudvisDawArrangement 45 | ] 46 | -------------------------------------------------------------------------------- /ui/props/lib.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/example-sk/audvis/35e7aa673bd9d57cfe9175937fca273fb4c194f7/ui/props/lib.py -------------------------------------------------------------------------------- /ui/props/midi.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | from .. import midi as ui_midi 4 | 5 | 6 | # midi_port = global_settings.GlobalSettings('midi_device') 7 | # midi_enable = global_settings.GlobalSettings('midi_enable', default=False) 8 | 9 | 10 | class AudvisMidiInputProperties(bpy.types.PropertyGroup): 11 | enable: bpy.props.BoolProperty(name="Enable", default=True) 12 | input_name: bpy.props.EnumProperty(name="Input Device", items=ui_midi.input_device_options) 13 | 14 | 15 | class AudvisMidiProperties(bpy.types.PropertyGroup): 16 | enable: bpy.props.BoolProperty(name="Enable Midi Realtime", default=False) 17 | list_index: bpy.props.IntProperty(name="List Index", default=1) 18 | inputs: bpy.props.CollectionProperty(name="Midi Inputs", type=AudvisMidiInputProperties) 19 | 20 | 21 | class AudvisMidiTrackProperties(bpy.types.PropertyGroup): 22 | enable: bpy.props.BoolProperty(name="Enable Midi Realtime", default=True) 23 | deleted: bpy.props.BoolProperty(name="Deleted", default=False) 24 | 25 | 26 | class AudvisMidiFileProperties(bpy.types.PropertyGroup): # custom properties for sequences are not animatable 27 | enable: bpy.props.BoolProperty(name="Enable", default=True) 28 | tracks: bpy.props.CollectionProperty(name="Midi Tracks", type=AudvisMidiTrackProperties) 29 | time_length: bpy.props.FloatProperty(name="Length in Seconds") 30 | fps_when_loaded: bpy.props.FloatProperty(name="FPS when loaded into .blend") 31 | list_index: bpy.props.IntProperty(name="List Index") 32 | filepath: bpy.props.StringProperty(name="Midi File Path") 33 | bpm: bpy.props.IntProperty(name="BPM") 34 | frame_start: bpy.props.IntProperty(name="Frame Start") 35 | animation_offset_start: bpy.props.IntProperty(name="Hold Offset Start", min=0) 36 | animation_offset_end: bpy.props.IntProperty(name="Hold Offset End", min=0) 37 | deleted: bpy.props.BoolProperty(name="Deleted", default=False) 38 | 39 | def fix_fps(self): 40 | scene = self.id_data 41 | old_fps = self.fps_when_loaded 42 | new_fps = scene.render.fps / scene.render.fps_base 43 | fps_ratio = new_fps / old_fps 44 | if new_fps == old_fps: 45 | return 46 | base_data_path = self.path_from_id() 47 | for fcurve in scene.animation_data.action.fcurves: 48 | if not fcurve.data_path.startswith(base_data_path): 49 | continue 50 | for point in fcurve.keyframe_points: 51 | point.co[0] *= fps_ratio 52 | fcurve.update() 53 | self.fps_when_loaded = new_fps 54 | 55 | 56 | class AudvisMidiFilesProperties(bpy.types.PropertyGroup): 57 | enable: bpy.props.BoolProperty(name="Enable", default=False) 58 | list_index: bpy.props.IntProperty(name="List Index") 59 | midi_files: bpy.props.CollectionProperty(name="Midi File List", type=AudvisMidiFileProperties) 60 | 61 | 62 | class AudvisMidiGeneratorsProperties(bpy.types.PropertyGroup): 63 | offset: bpy.props.IntProperty(name="MIDI Note Offset", default=0) 64 | file: bpy.props.StringProperty(name="MIDI FIle") 65 | track: bpy.props.StringProperty(name="MIDI Track", default='') 66 | channel: bpy.props.EnumProperty(name="MIDI Channel", 67 | default='all', 68 | items=[('all', 'All', '')] + [(str(i + 1), str(i + 1), "") for i in range(16)]) 69 | device: bpy.props.StringProperty(name="MIDI Input Device") 70 | 71 | 72 | classes = [ 73 | AudvisMidiGeneratorsProperties, 74 | AudvisMidiTrackProperties, 75 | AudvisMidiFileProperties, 76 | AudvisMidiFilesProperties, 77 | AudvisMidiInputProperties, 78 | AudvisMidiProperties, 79 | ] 80 | -------------------------------------------------------------------------------- /ui/props/obj.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | from . import ( 4 | armaturegenerator, 5 | shapemodifier, 6 | ) 7 | 8 | 9 | class AudvisObjectProperties(bpy.types.PropertyGroup): 10 | shapemodifier: bpy.props.PointerProperty(type=shapemodifier.AudvisObjectShapemodifierProperties) 11 | armature_generator: bpy.props.PointerProperty(type=armaturegenerator.AudvisObjectArmatureGeneratorProperties) 12 | -------------------------------------------------------------------------------- /ui/props/party.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | impl_window = ( 4 | 'window', 5 | 'Window', 6 | 'Opens temporary window and maximizes it. Use if you want to control Blender from main window.' 7 | ) 8 | impl_workspace = ( 9 | 'workspace', 10 | 'Workspace', 11 | 'Adds a workspace. Use for higher FPS.' 12 | ) 13 | 14 | if bpy.app.version[0] < 4: 15 | _supported_implementations = [impl_window, impl_workspace] 16 | else: 17 | _supported_implementations = [impl_workspace] 18 | 19 | 20 | class AudvisPartyProperties(bpy.types.PropertyGroup): 21 | show_xray: bpy.props.BoolProperty(name="Show X-Ray for Wireframe in Party Mode", default=False) 22 | hide_mouse: bpy.props.BoolProperty(name="Hide Mouse Pointer", default=False) 23 | shading: bpy.props.EnumProperty(name="Shading in Party Mode", items=[ 24 | ('WIREFRAME', 'Wireframe', ''), 25 | ('SOLID', 'Solid', ''), 26 | ('MATERIAL', 'Material', ''), 27 | ('RENDERED', 'Rendered', ''), 28 | ], default='RENDERED') 29 | implementation: bpy.props.EnumProperty(name="Implementation of Party Mode", items=_supported_implementations) 30 | -------------------------------------------------------------------------------- /ui/props/realtimeprops.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | 4 | class AudvisRealtimeItemProperties(bpy.types.PropertyGroup): 5 | enable: bpy.props.BoolProperty(name="Realtime Source Enable", default=True) 6 | device_name: bpy.props.StringProperty(name="Device Name") 7 | uuid: bpy.props.StringProperty() 8 | 9 | 10 | class AudvisRealtimeMultiProperties(bpy.types.PropertyGroup): 11 | enable: bpy.props.BoolProperty(name="Multiple Sources Enable", default=False) 12 | list: bpy.props.CollectionProperty(type=AudvisRealtimeItemProperties) 13 | index: bpy.props.IntProperty(default=0) 14 | collection: bpy.props.PointerProperty(type=bpy.types.Collection) 15 | 16 | 17 | classes = [ 18 | AudvisRealtimeItemProperties, 19 | AudvisRealtimeMultiProperties 20 | ] 21 | -------------------------------------------------------------------------------- /ui/props/sequence.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | 4 | class AudvisSequenceProperties(bpy.types.PropertyGroup): 5 | enable: bpy.props.BoolProperty(name="Enable Audio Visualizer", default=True) 6 | -------------------------------------------------------------------------------- /ui/props/shapemodifier.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | from .midi import AudvisMidiGeneratorsProperties 4 | from .. import shapemodifier 5 | from ...analyzer.shapemodifier import create_shapemodifier_vertgroup 6 | 7 | 8 | class AudvisObjectShapemodifierProperties(bpy.types.PropertyGroup): 9 | enable: bpy.props.BoolProperty(name="Enable", default=False) 10 | order: bpy.props.EnumProperty(name="Order", items=shapemodifier.order_enum) 11 | channel: bpy.props.IntProperty(name="Sound Channel", default=1, min=1, soft_max=32) 12 | random_seed: bpy.props.IntProperty(name="Randomize Seed", default=1) 13 | freq_seq_type: bpy.props.EnumProperty(name="Freq Sequencing", items=[ 14 | ("classic", "Linear", "0-50 ; 50-100 ; 100-150..."), 15 | ("notes", "Notes", "Music notes"), 16 | ("midi", "MIDI Notes", "MIDI notes (limited to 127 points)"), 17 | ]) 18 | note_a4_freq: bpy.props.FloatProperty(name="A4 Note Frequency", default=440.0, soft_min=432.0, soft_max=446.0) 19 | note_step: bpy.props.FloatProperty(name="Note Step", default=1, step=50) 20 | note_offset: bpy.props.IntProperty(name="Note Steps Offset", default=0) 21 | 22 | midi: bpy.props.PointerProperty(name="MIDI", type=AudvisMidiGeneratorsProperties) 23 | 24 | freqrange: bpy.props.FloatProperty(name="Frequency Range Per Point", default=50, min=.01) 25 | freqstart: bpy.props.FloatProperty(name="Frequency Start", default=0, min=0) 26 | freq_step_enable: bpy.props.BoolProperty(name="Set Custom Step", default=False) 27 | freq_step: bpy.props.FloatProperty(name="Frequency Step", default=5.0, min=0) 28 | freq_step_calc: bpy.props.FloatProperty(name="Frequency Step Calculated", get=shapemodifier.calc_freq_step) 29 | animtype: bpy.props.EnumProperty(name="Animation Type", items=shapemodifier.animation_type_enum) 30 | additive: bpy.props.EnumProperty(name="Additive", default="off", items=[ 31 | ('off', 'Off', ''), 32 | ('on', 'On', ''), 33 | ('sin', 'Sine from -1 to 1', ''), 34 | ('sin2', 'Sine from 0 to 1', ''), 35 | ('mod', 'Modulo', ''), 36 | ]) 37 | additive_modulus: bpy.props.FloatProperty(name="Modulus", default=1.0) 38 | additive_phase_multiplier: bpy.props.FloatProperty(name="Phase Multiplier", default=1.0) 39 | additive_phase_offset: bpy.props.FloatProperty(name="Phase Offset", default=0.0) 40 | factor: bpy.props.FloatProperty(name="Factor", default=.1, precision=4) 41 | add: bpy.props.FloatProperty(name="Add", default=0, precision=4) 42 | use_vertexgroup: bpy.props.BoolProperty(name="Use Vertex Group", default=False, 43 | update=create_shapemodifier_vertgroup) 44 | gpencil_layer: bpy.props.StringProperty(name="Layer", default="", update=shapemodifier.gpencil_layer_changed) 45 | gpencil_layer_changed: bpy.props.BoolProperty(name="GPencil Layer Value Changed", default=False) 46 | vector: bpy.props.FloatVectorProperty(name="Vector", default=[0, 0, 1], subtype='TRANSLATION') 47 | uv_vector: bpy.props.FloatVectorProperty(name="UV", default=[1, 0], size=2) 48 | vertcolor_color: bpy.props.FloatVectorProperty(name="Vertex Color", default=[1, 1, 1], size=3, subtype='COLOR') 49 | operation: bpy.props.EnumProperty(name="Operation Type", items=[ 50 | ('add', 'Add', 'add'), 51 | ('set', 'Set', 'set'), 52 | ]) 53 | track_object: bpy.props.PointerProperty(type=bpy.types.Object, name="Target") 54 | is_baking: bpy.props.BoolProperty(name="Enable", default=False) 55 | sound_sequence: bpy.props.StringProperty(name="Sequence", default="") 56 | sequence_channel: bpy.props.IntProperty(name="Sequence Channel", default=0, min=0, 57 | description="Channel number in Video Sequence Editor") 58 | -------------------------------------------------------------------------------- /ui/props/spectrogram.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | from .midi import AudvisMidiGeneratorsProperties 4 | from .. import spectrogram 5 | 6 | 7 | class AudvisSpectrogramMetaProperties(bpy.types.PropertyGroup): 8 | enable: bpy.props.BoolProperty(name="Spectrogram Enable", default=True) 9 | mode: bpy.props.EnumProperty(name="Spectrogram Mode", items=[ 10 | ("single", "Single", "Only one image (old behavior)"), 11 | ("multi", "Multiple", 12 | "Create multiple spectrogram images (warning: having too many spectrogram images can be slow)"), 13 | ]) 14 | index: bpy.props.IntProperty(min=0) 15 | bake_dirname: bpy.props.StringProperty(name="Bake Directory", subtype="DIR_PATH", default="//audvis-spectrogram/") 16 | bake_use_subdirs: bpy.props.BoolProperty(name="Use Subdirs", default=True) 17 | bake_disable_after: bpy.props.BoolProperty(name="Disable after Baking", default=True) 18 | bake_show_directory: bpy.props.BoolProperty(name="Show Directory", default=True) 19 | 20 | class AudvisSpectrogramProperties(bpy.types.PropertyGroup): 21 | enable: bpy.props.BoolProperty(name="Enable", default=False) 22 | channel: bpy.props.IntProperty(name="Sound Channel", default=1, min=1, soft_max=32) 23 | random_seed: bpy.props.IntProperty(name="Randomize Seed", default=1) 24 | freq_seq_type: bpy.props.EnumProperty(name="Freq Sequencing", items=[ 25 | ("classic", "Linear", "0-50 ; 50-100 ; 100-150..."), 26 | ("notes", "Notes", "Music notes"), 27 | ("midi", "MIDI Notes", "MIDI notes (limited to 127 points)"), 28 | ]) 29 | note_a4_freq: bpy.props.FloatProperty(name="A4 Note Frequency", default=440.0, soft_min=432.0, soft_max=446.0) 30 | note_step: bpy.props.FloatProperty(name="Note Step", default=1, step=50) 31 | note_offset: bpy.props.IntProperty(name="Note Steps Offset", default=0) 32 | 33 | midi: bpy.props.PointerProperty(name="MIDI", type=AudvisMidiGeneratorsProperties) 34 | 35 | freqrange: bpy.props.FloatProperty(name="Frequency Range Per Point", default=50, min=.01) 36 | freqstart: bpy.props.FloatProperty(name="Frequency Start", default=0, min=0) 37 | freq_step_enable: bpy.props.BoolProperty(name="Set Custom Step", default=False) 38 | freq_step: bpy.props.FloatProperty(name="Frequency Step", default=5.0, min=0) 39 | freqstart: bpy.props.FloatProperty(name="Frequency Start", default=0, min=0) 40 | freq_step_calc: bpy.props.FloatProperty(name="Frequency Step Calculated", get=spectrogram.calc_freq_step) 41 | sound_sequence: bpy.props.StringProperty(name="Sequence", default="") 42 | sequence_channel: bpy.props.IntProperty(name="Sequence Channel", default=0, min=0, 43 | description="Channel number in Video Sequence Editor") 44 | width: bpy.props.IntProperty(name="Image Width", default=100, soft_max=300, min=1, soft_min=10) 45 | height: bpy.props.IntProperty(name="Image Height", default=100, soft_max=300, min=1, soft_min=10) 46 | mode: bpy.props.EnumProperty(name="Mode", items=[ 47 | ("rolling", "Rolling", "One image with selected height. Current line is always on the bottom of the image."), 48 | ("one-big", "One Image", "Makes one big image with height the same as the scene's frame count." 49 | " Current line is on the current frame's number," 50 | " counted from the bottom of the image."), 51 | ("snapshot", "Snapshot", "Each pixel is set from the latest data") 52 | ], default="rolling") 53 | onebig_force_vertical: bpy.props.BoolProperty("Vertical Only", default=False, description="Use with particle textures") 54 | image: bpy.props.PointerProperty(type=bpy.types.Image) 55 | factor_float: bpy.props.FloatProperty(name="Factor", default=1) 56 | factor: bpy.props.FloatVectorProperty(name="Factor RGB", size=3, soft_min=-1, soft_max=1, default=(1, 1, 1)) 57 | bake_path: bpy.props.StringProperty(name="Bake Path", default="//audvis-spectrogram/", subtype='DIR_PATH') 58 | clear_on_first_frame: bpy.props.BoolProperty(name="Clear on First Frame", default=True) 59 | color: bpy.props.FloatVectorProperty( 60 | name="Base Color", 61 | subtype="COLOR", 62 | size=4, 63 | min=0.0, 64 | max=1.0, 65 | default=(0.0, 0.0, 0.0, 1.0)) 66 | operation: bpy.props.EnumProperty(name="Operation Type", items=[ 67 | ('add', 'Add', 'add'), 68 | ('sub', 'Subtract', ''), 69 | ('mul', 'Multiply', ''), 70 | ('set', 'Set', ''), 71 | ]) 72 | skip_frames: bpy.props.IntProperty(name="Skip Frames", default=0, min=0) 73 | # TODO: support png and targa raw, maybe other formats for baking 74 | -------------------------------------------------------------------------------- /ui/props/spreaddrivers.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from .midi import AudvisMidiGeneratorsProperties 3 | 4 | class AudvisSceneSpreaddriversProperties(bpy.types.PropertyGroup): 5 | iteration: bpy.props.IntProperty(name="Iteration", default=0, soft_min=0) 6 | factor: bpy.props.FloatProperty(name="Factor", default=1) 7 | add: bpy.props.FloatProperty(name="Add Value", default=0) 8 | freqrange: bpy.props.FloatProperty(name="Frequency Range Per Iteration", default=50) 9 | freqstart: bpy.props.FloatProperty(name="Frequency Start", default=0, min=0) 10 | additive: bpy.props.BoolProperty(name="Additive", default=False) 11 | 12 | freq_step_enable: bpy.props.BoolProperty(name="Set Custom Step", default=False) 13 | freq_step: bpy.props.FloatProperty(name="Frequency Step ", default=5.0, soft_min=0, soft_max=1000, 14 | description="If set to 5, objects will use" 15 | " frequencies 0-50, 5-55, 10-60...") 16 | channel: bpy.props.IntProperty(name="Sound Channel", default=1, min=1, soft_max=32) 17 | sound_sequence: bpy.props.StringProperty(name="Sequence") 18 | sequence_channel: bpy.props.IntProperty(name="Sequence Channel", default=0, min=0, 19 | description="Channel number in Video Sequence Editor") 20 | freq_seq_type: bpy.props.EnumProperty(name="Freq Sequencing", items=[ 21 | ("classic", "Linear", "0-50 ; 50-100 ; 100-150..."), 22 | ("notes", "Notes", "Music notes"), 23 | ("midi", "MIDI Notes", "MIDI notes (limited to 127 points)"), 24 | ]) 25 | note_a4_freq: bpy.props.FloatProperty(name="A4 Note Frequency", default=440.0, soft_min=432.0, soft_max=446.0) 26 | note_step: bpy.props.FloatProperty(name="Note Step", default=1, step=50) 27 | note_offset: bpy.props.IntProperty(name="Note Steps Offset", default=0) 28 | midi: bpy.props.PointerProperty(name="MIDI", type=AudvisMidiGeneratorsProperties) 29 | expression: bpy.props.StringProperty(name="Expression", default="audvis()") 30 | -------------------------------------------------------------------------------- /ui/props/valuesaud.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | from ...analyzer.analyzer import make_fake_curve_light 4 | 5 | 6 | class AudvisValuesAudProperties(bpy.types.PropertyGroup): 7 | enable: bpy.props.BoolProperty(name="Use Better Filters") 8 | highpass: bpy.props.FloatProperty(name="Highpass Frequency", default=0, min=0) 9 | highpass_factor: bpy.props.FloatProperty(name="Q", default=.5, min=0.00001, max=1) 10 | lowpass: bpy.props.FloatProperty(name="Lowpass Frequency", default=100000, min=0) 11 | lowpass_factor: bpy.props.FloatProperty(name="Q", default=.5, min=0.00001, max=1) 12 | fake_curve_light: bpy.props.PointerProperty(type=bpy.types.Light, name="Fake Curve Light") 13 | use_fake_curve_light: bpy.props.BoolProperty(name="Use Curve", default=False, update=make_fake_curve_light, 14 | description="Caution: this is quite slow") 15 | # aud.ADSR params 16 | adsr_enable: bpy.props.BoolProperty(name="ADSR", default=True) 17 | adsr_attack: bpy.props.FloatProperty(name="Attack", default=0.1, min=0, precision=3, step=1) 18 | adsr_decay: bpy.props.FloatProperty(name="Decay", default=0.01, min=0, precision=3, step=1) 19 | adsr_sustain: bpy.props.FloatProperty(name="Sustain", min=0, precision=3, step=1) 20 | adsr_release: bpy.props.FloatProperty(name="Release", default=0.200, min=0, precision=3, step=1) 21 | # aud.envelope params 22 | env_enable: bpy.props.BoolProperty(name="Envelope", default=False) 23 | env_attack: bpy.props.FloatProperty(name="Attack", default=0.005, min=0, precision=3) 24 | env_release: bpy.props.FloatProperty(name="Release", min=0, default=.2) 25 | env_threshold: bpy.props.FloatProperty(name="Threshold", min=0, default=0) 26 | env_arthreshold: bpy.props.FloatProperty(name="Attack/Release Threshold", default=0.100, min=0) 27 | -------------------------------------------------------------------------------- /ui/realtime/recording.py: -------------------------------------------------------------------------------- 1 | import math 2 | import sys 3 | 4 | import bpy 5 | 6 | from ..buttonspanel import AudVisButtonsPanel_Npanel 7 | 8 | 9 | def _get_audvis(): 10 | return bpy.audvis 11 | 12 | 13 | def _poll(): 14 | audvis = _get_audvis() 15 | if audvis.realtime_analyzer is None: 16 | return False 17 | if audvis.realtime_analyzer.recorder is None: 18 | return False 19 | if audvis.realtime_analyzer.recorder.status == audvis.realtime_analyzer.recorder.STATUS_RUNNING: 20 | return True 21 | return False 22 | 23 | 24 | class AUDVIS_OT_RealtimeRecordStart(bpy.types.Operator): 25 | bl_idname = "audvis.realtime_record_start" 26 | bl_label = "Start recording sound from sound card" 27 | bl_description = "Enable realtime analyzer to make this work" 28 | 29 | @classmethod 30 | def poll(cls, context): 31 | return context.scene.audvis.realtime_enable and not _poll() 32 | 33 | def execute(self, context): 34 | audvis = _get_audvis() 35 | audvis.update_data(context.scene) 36 | audvis.realtime_analyzer.recorder.start() 37 | return {'FINISHED'} 38 | 39 | 40 | class AUDVIS_OT_RealtimeRecordStop(bpy.types.Operator): 41 | bl_idname = "audvis.realtime_record_stop" 42 | bl_label = "Stop recording sound from sound card" 43 | 44 | @classmethod 45 | def poll(cls, context): 46 | return _poll() 47 | 48 | def execute(self, context): 49 | audvis = _get_audvis() 50 | path = audvis.realtime_analyzer.recorder_stop(bpy.app.tempdir, context.scene.audvis.realtime_save_format) 51 | if context.scene.audvis.realtime_loadassequence: 52 | context.scene.sequence_editor_create() # ensure sequence_editor exists 53 | sequence = context.scene.sequence_editor.sequences.new_sound(name="AudVis Record", filepath=path, channel=1, 54 | frame_start=context.scene.frame_start) 55 | if context.scene.audvis.realtime_save_pack: 56 | sequence.sound.pack() 57 | return {'FINISHED'} 58 | 59 | 60 | def get_available_formats(self, context): 61 | # TODO: cache 62 | try: 63 | import soundfile 64 | lst = [] 65 | formats = soundfile.available_formats() 66 | for key in formats: 67 | lst.append((key, formats[key], key)) 68 | return lst 69 | except: 70 | return [] 71 | 72 | 73 | class AUDVIS_PT_realtimeRecordNpanel(AudVisButtonsPanel_Npanel): 74 | bl_parent_id = "AUDVIS_PT_realtimeNpanel" 75 | bl_options = {'DEFAULT_CLOSED'} 76 | bl_label = "Record Audio" 77 | 78 | @classmethod 79 | def poll(cls, context): 80 | return bpy.audvis.is_realtime_supported() 81 | 82 | def draw(self, context): 83 | col = self.layout.column(align=True) 84 | if bpy.audvis.is_recording_supported(): 85 | if bpy.audvis.realtime_analyzer is None: 86 | # bpy.audvis.update_data(context.scene) # wrong context 87 | pass 88 | try: 89 | seconds = bpy.audvis.realtime_analyzer.recorder.seconds 90 | except: 91 | seconds = 0 92 | minutes = math.floor(seconds / 60) 93 | col.prop(context.scene.audvis, 'realtime_save_format') 94 | col.prop(context.scene.audvis, 'realtime_loadassequence') 95 | if context.scene.audvis.realtime_loadassequence: 96 | col.prop(context.scene.audvis, 'realtime_save_pack') 97 | col.operator("audvis.realtime_record_start", text="start recording") 98 | col.operator("audvis.realtime_record_stop", text="stop recording") 99 | col.label(text="Recorded: %02d:%02d" % (minutes, math.floor(seconds - minutes * 60))) 100 | else: 101 | col.operator("audvis.install", text="Install python packages") 102 | 103 | 104 | classes = [ 105 | AUDVIS_OT_RealtimeRecordStart, 106 | AUDVIS_OT_RealtimeRecordStop, 107 | AUDVIS_PT_realtimeRecordNpanel, 108 | ] 109 | -------------------------------------------------------------------------------- /ui/scripttemplates.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import os 3 | from bpy.types import Menu 4 | 5 | from .buttonspanel import AudVisButtonsPanel_Npanel 6 | 7 | 8 | class AUDVIS_MT_scripttemplates(Menu): 9 | bl_idname = "AUDVIS_MT_scripttemplates" 10 | bl_label = "AudVis Script Templates" 11 | 12 | def draw(self, _context): 13 | self.path_menu( 14 | [os.path.join(os.path.dirname(os.path.dirname(__file__)), "doc", "scripts")], 15 | "text.open", 16 | props_default={"internal": True}, 17 | ) 18 | 19 | def draw_mainmenu(self, _context): # static 20 | self.layout.menu("AUDVIS_MT_scripttemplates") 21 | 22 | 23 | class AUDVIS_OT_scriptmenuopen(bpy.types.Operator): 24 | bl_idname = "audvis.scriptmenuopen" 25 | bl_label = "AudVis Script Template" 26 | 27 | def execute(self, context): 28 | bpy.ops.wm.call_menu(name="AUDVIS_MT_scripttemplates") 29 | return {"FINISHED"} 30 | 31 | 32 | class AUDVIS_PT_examplescriptsNpanel(AudVisButtonsPanel_Npanel): 33 | bl_label = "Script Templates" 34 | 35 | scriptlist = None 36 | 37 | @classmethod 38 | def poll(cls, context): 39 | return True 40 | 41 | def draw(self, context): 42 | col = self.layout.column(align=True) 43 | col.label(text="Please Switch to") 44 | col.label(text="Scripting Workspace") 45 | col.operator("audvis.scriptmenuopen", text="Choose Script Template") 46 | 47 | 48 | def register(): 49 | if hasattr(bpy.types, 'TEXT_MT_templates'): 50 | bpy.types.TEXT_MT_templates.append(AUDVIS_MT_scripttemplates.draw_mainmenu) 51 | 52 | 53 | def unregister(): 54 | if hasattr(bpy.types, 'TEXT_MT_templates'): 55 | bpy.types.TEXT_MT_templates.remove(AUDVIS_MT_scripttemplates.draw_mainmenu) 56 | 57 | 58 | classes = [ 59 | AUDVIS_PT_examplescriptsNpanel, 60 | AUDVIS_MT_scripttemplates, 61 | AUDVIS_OT_scriptmenuopen 62 | ] 63 | -------------------------------------------------------------------------------- /ui/spectrogram/operators/spectrogramAdd.py: -------------------------------------------------------------------------------- 1 | import random 2 | import bpy 3 | import string 4 | from bpy.types import Operator 5 | 6 | 7 | class AUDVIS_OT_spectrogramAdd(Operator): 8 | bl_idname = "audvis.spectrogram_add" 9 | bl_label = "Add Spectrogram" 10 | 11 | def execute(self, context): 12 | spectrogram = context.scene.audvis.spectrograms.add() 13 | spectrogram.enable = True 14 | name = "spectrogram " + ''.join(random.choice(string.ascii_letters) for i in range(4)) 15 | spectrogram.image = bpy.data.images.new(name=name, width=spectrogram.width, height=spectrogram.height) 16 | spectrogram.image.source = 'GENERATED' 17 | if hasattr(spectrogram.image, "use_half_precision"): 18 | spectrogram.image.use_half_precision = False 19 | spectrogram.name = spectrogram.image.name 20 | context.scene.audvis.spectrogram_meta.index = len(context.scene.audvis.spectrograms) - 1 21 | return {"FINISHED"} 22 | -------------------------------------------------------------------------------- /ui/spectrogram/operators/spectrogramDuplicate.py: -------------------------------------------------------------------------------- 1 | from bpy.types import Operator 2 | 3 | 4 | class AUDVIS_OT_spectrogramDuplicate(Operator): 5 | bl_idname = "audvis.spectrogram_duplicate" 6 | bl_label = "Duplicate Spectrogram" 7 | 8 | @classmethod 9 | def poll(cls, context): 10 | return context.scene.audvis.spectrogram_meta.index < len(context.scene.audvis.spectrograms) 11 | 12 | def execute(self, context): 13 | old_spectrogram = context.scene.audvis.spectrograms[context.scene.audvis.spectrogram_meta.index] 14 | new_spectrogram = context.scene.audvis.spectrograms.add() 15 | for key, value in list(old_spectrogram.items()): 16 | setattr(new_spectrogram, key, getattr(old_spectrogram, key)) 17 | new_spectrogram.image = new_spectrogram.image.copy() 18 | new_spectrogram.name = new_spectrogram.image.name 19 | context.scene.audvis.spectrogram_meta.index = len(context.scene.audvis.spectrograms) - 1 20 | return {"FINISHED"} 21 | -------------------------------------------------------------------------------- /ui/spectrogram/operators/spectrogramMakeParticles.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.types import ( 3 | Operator, 4 | ) 5 | 6 | 7 | def _get_spect_props(context): 8 | if context.scene.audvis.spectrogram_meta.mode == 'single': 9 | return context.scene.audvis.spectrogram 10 | else: 11 | if context.scene.audvis.spectrogram_meta.index < len(context.scene.audvis.spectrograms): 12 | return context.scene.audvis.spectrograms[context.scene.audvis.spectrogram_meta.index] 13 | 14 | 15 | class AUDVIS_OT_spectrogramMakeParticles(Operator): 16 | bl_idname = "audvis.spectrogram_make_particles" 17 | bl_label = "Make Particle System" 18 | 19 | @classmethod 20 | def poll(cls, context): 21 | if not context.object or context.object.type not in ('MESH',): 22 | return False 23 | if not bpy.ops.object.particle_system_add.poll(): 24 | return False 25 | if context.scene.audvis.spectrogram_meta.mode != 'multi': 26 | return False 27 | return True 28 | 29 | def execute(self, context): 30 | bpy.ops.audvis.spectrogram_add() 31 | spect_props = context.scene.audvis.spectrograms[-1] 32 | spect_props.mode = 'one-big' 33 | spect_props.width = 1 34 | bpy.ops.object.particle_system_add() 35 | psystem = context.object.particle_systems[-1] 36 | 37 | psettings = psystem.settings 38 | psystem.name = 'audvis' 39 | psettings.name = 'audvis' 40 | psettings.count = 3003 41 | psettings.frame_start = context.scene.frame_start 42 | psettings.frame_end = context.scene.frame_end 43 | psettings.render_type = 'OBJECT' 44 | slot = psettings.texture_slots.add() 45 | slot.texture = bpy.data.textures.new(name='audvis', type='IMAGE') 46 | slot.texture.extension = 'EXTEND' 47 | slot.texture.image = spect_props.image 48 | slot.use_map_time = False 49 | slot.use_map_size = True 50 | slot.texture_coords = 'STRAND' 51 | bpy.ops.mesh.primitive_cube_add(location=(0, 10, 0)) 52 | render_obj = bpy.context.object 53 | psettings.instance_object = render_obj 54 | return {'FINISHED'} 55 | -------------------------------------------------------------------------------- /ui/spectrogram/operators/spectrogramRemove.py: -------------------------------------------------------------------------------- 1 | from bpy.types import Operator 2 | 3 | 4 | class AUDVIS_OT_spectrogramRemove(Operator): 5 | bl_idname = "audvis.spectrogram_remove" 6 | bl_label = "Remove Spectrogram" 7 | 8 | @classmethod 9 | def poll(cls, context): 10 | return context.scene.audvis.spectrogram_meta.index < len(context.scene.audvis.spectrograms) 11 | 12 | def execute(self, context): 13 | audvis_props = context.scene.audvis 14 | index = audvis_props.spectrogram_meta.index 15 | # spect_props = audvis_props.spectrograms[index] 16 | audvis_props.spectrograms.remove(index) 17 | if len(audvis_props.spectrograms) == 0: 18 | audvis_props.spectrogram_meta.index = 0 19 | elif index >= len(audvis_props.spectrograms): 20 | audvis_props.spectrogram_meta.index = len(audvis_props.spectrograms) - 1 21 | return {"FINISHED"} 22 | -------------------------------------------------------------------------------- /ui/spectrogram/operators/spectrogramSingleToMulti.py: -------------------------------------------------------------------------------- 1 | from bpy.types import Operator 2 | import bpy 3 | 4 | 5 | class AUDVIS_OT_spectrogramSingleToMulti(Operator): 6 | bl_idname = "audvis.spectrogram_single_to_multi" 7 | bl_label = "Copy Single Spectrogram Settings into Multi" 8 | 9 | def execute(self, context): 10 | old_spectrogram = context.scene.audvis.spectrogram 11 | new_spectrogram = context.scene.audvis.spectrograms.add() 12 | name = "copy of single spectrogram" 13 | for key, value in list(old_spectrogram.items()): 14 | setattr(new_spectrogram, key, getattr(old_spectrogram, key)) 15 | if old_spectrogram.image: 16 | new_spectrogram.image = new_spectrogram.image.copy() 17 | new_spectrogram.name = new_spectrogram.image.name 18 | else: 19 | spectrogram = new_spectrogram 20 | spectrogram.image = bpy.data.images.new(name=name, width=spectrogram.width, height=spectrogram.height) 21 | spectrogram.image.source = 'GENERATED' 22 | if hasattr(spectrogram.image, "use_half_precision"): 23 | spectrogram.image.use_half_precision = False 24 | spectrogram.name = spectrogram.image.name 25 | 26 | context.scene.audvis.spectrogram_meta.index = len(context.scene.audvis.spectrograms) - 1 27 | context.scene.audvis.spectrogram_meta.mode = 'multi' 28 | return {"FINISHED"} 29 | -------------------------------------------------------------------------------- /ui/spread_drivers/__init__.py: -------------------------------------------------------------------------------- 1 | from . import ( 2 | spread_drivers_ui, 3 | menuitem, 4 | ) 5 | 6 | classes = spread_drivers_ui.classes \ 7 | + menuitem.classes 8 | 9 | 10 | def register(): 11 | menuitem.WM_MT_button_context.append(menuitem.menu_func) 12 | 13 | 14 | def unregister(): 15 | menuitem.WM_MT_button_context.remove(menuitem.menu_func) 16 | -------------------------------------------------------------------------------- /ui/spread_drivers/expression_builder.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from ...note_calculator import calculate_note 4 | 5 | 6 | def build_expression(props, add=0.0, iterations_add=0): 7 | rangeperobject = props.freqrange 8 | freqstart = props.freqstart 9 | if props.freq_step_enable: 10 | freq_step = props.freq_step 11 | else: 12 | freq_step = rangeperobject 13 | index = props.iteration + iterations_add 14 | if props.freq_seq_type == 'midi': 15 | expr_kwargs = 'midi={}'.format((index + props.midi.offset) % 128) 16 | if props.midi.file != '': 17 | expr_kwargs += ', file=' + repr(props.midi.file) 18 | if props.midi.track != '': 19 | expr_kwargs += ', track=' + repr(props.midi.track) 20 | if props.midi.channel != 'all' and props.midi.channel != '': 21 | expr_kwargs += ', ch=' + repr(props.midi.channel) 22 | if props.midi.device != '': 23 | expr_kwargs += ', device=' + repr(props.midi.device) 24 | args = expr_kwargs 25 | else: 26 | if props.freq_seq_type == 'notes': 27 | freq_from = calculate_note((index + props.note_offset) * props.note_step, 28 | props.note_a4_freq) 29 | freq_to = calculate_note((index + props.note_offset + 1) * props.note_step, 30 | props.note_a4_freq) 31 | else: 32 | freq_from = index * freq_step + freqstart 33 | freq_to = freq_from + rangeperobject 34 | args = "{:.6}, {:.6}".format(freq_from, freq_to) 35 | if props.sound_sequence != '': 36 | args += ', seq=' + repr(props.sound_sequence) 37 | if props.sequence_channel > 0: 38 | args += ', seq_channel=' + str(props.sequence_channel) 39 | if props.channel != 1: 40 | args += ', ch={}'.format(props.channel) 41 | if props.additive: 42 | args += ', additive=True' 43 | 44 | main_expr = "audvis({})".format(args) 45 | if props.factor != 1.0: 46 | main_expr += " * {:.6}".format(props.factor) 47 | 48 | result = props.expression 49 | result = re.sub(r'\bindex\b', str(props.iteration), result) 50 | result = result.replace("audvis()", main_expr) 51 | if add + props.add != 0.0: 52 | result = "{:.6} + {}".format((add + props.add) * 1.0, result) 53 | return result 54 | -------------------------------------------------------------------------------- /ui/spread_drivers/menuitem.py: -------------------------------------------------------------------------------- 1 | import re 2 | import token 3 | from io import BytesIO 4 | from tokenize import tokenize 5 | 6 | import bpy 7 | from bpy.types import Menu 8 | 9 | from .expression_builder import build_expression 10 | 11 | 12 | def parse_path(path): 13 | g = tokenize(BytesIO(path.encode("utf-8")).readline) 14 | tokens = [item.string for item in g if (item.string and item.type != token.ENCODING)] 15 | if tokens[-1] == ']': 16 | return "".join(tokens[:-3]), "".join(tokens[-3:]) 17 | else: 18 | return "".join(tokens[:-2]), tokens[-1] 19 | 20 | 21 | class AUDVIS_OT_button_spread_driver(bpy.types.Operator): 22 | """Right click entry test""" 23 | bl_idname = "audvis.button_spread_driver" 24 | bl_label = "AudVis: Spread Drivers" 25 | 26 | @classmethod 27 | def poll(cls, context): 28 | return bpy.ops.ui.copy_data_path_button.poll() and bpy.ops.anim.driver_button_add.poll() 29 | 30 | def _get_path(self, context): 31 | clipboard_old = context.window_manager.clipboard 32 | bpy.ops.ui.copy_data_path_button(full_path=True) 33 | full_path = context.window_manager.clipboard 34 | context.window_manager.clipboard = clipboard_old 35 | return full_path # , property_path 36 | 37 | def execute(self, context): 38 | # pointer = getattr(context, "button_pointer", None) 39 | # prop = getattr(context, "button_prop", None) 40 | # operator = getattr(context, "button_operator", None) 41 | orig_path = self._get_path(context) 42 | index = -1 43 | match = re.match(r'^(.*)\[([0-9]+)\]$', orig_path) 44 | path = orig_path 45 | if match is not None: 46 | path = match.group(1) 47 | index = int(match.group(2)) 48 | path_to_struct, property_path = parse_path(path) 49 | struct = eval(path_to_struct) 50 | fcurve = struct.driver_add(property_path, index) 51 | props = context.scene.audvis.spreaddrivers 52 | 53 | old_value = eval(orig_path) 54 | fcurve.driver.expression = build_expression(props, old_value) 55 | props.iteration += 1 56 | return {'FINISHED'} 57 | 58 | 59 | # This class has to be exactly named like that to insert an entry in the right click menu 60 | class WM_MT_button_context(Menu): 61 | bl_label = "Unused" 62 | 63 | def draw(self, context): 64 | pass 65 | 66 | 67 | def menu_func(self, context): 68 | if bpy.ops.audvis.button_spread_driver.poll(): 69 | layout = self.layout 70 | layout.separator() 71 | layout.operator(AUDVIS_OT_button_spread_driver.bl_idname) 72 | 73 | 74 | classes = [ 75 | AUDVIS_OT_button_spread_driver, 76 | WM_MT_button_context, 77 | ] 78 | -------------------------------------------------------------------------------- /ui/spread_drivers/spread_drivers_ui.py: -------------------------------------------------------------------------------- 1 | from .expression_builder import build_expression 2 | from .. import ui_lib 3 | from ..buttonspanel import AudVisButtonsPanel_Npanel 4 | 5 | 6 | class AUDVIS_PT_spreaddriversNpanel(AudVisButtonsPanel_Npanel): 7 | bl_label = "Spread the Drivers" 8 | 9 | @classmethod 10 | def poll(cls, context): 11 | return True 12 | 13 | def draw(self, context): 14 | scene = context.scene 15 | props = scene.audvis.spreaddrivers 16 | col = self.layout.column(align=True) 17 | col.prop(props, "freq_seq_type") 18 | if props.freq_seq_type == "classic": 19 | col.prop(props, "freqrange") 20 | col.prop(props, "freqstart") 21 | col.prop(props, "freq_step_enable") 22 | if props.freq_step_enable: 23 | col.prop(props, "freq_step") 24 | elif props.freq_seq_type == "notes": 25 | col.prop(props, "note_a4_freq") 26 | col.prop(props, "note_step") 27 | col.prop(props, "note_offset") 28 | elif props.freq_seq_type == "midi": 29 | col.prop(props.midi, "offset") 30 | 31 | col.prop(props, "factor") 32 | col.prop(props, "add") 33 | if props.freq_seq_type != "midi": 34 | col.prop(props, "additive") 35 | col.prop(props, "freq_seq_type") 36 | if props.freq_seq_type == "midi": 37 | ui_lib.generators_ui_midi(self, context, props.midi) 38 | else: 39 | ui_lib.generators_ui_sequence(self, context, props) 40 | col = self.layout.column(align=True) 41 | col.prop(props, "expression") 42 | col.prop(props, "iteration") 43 | col.label(text="Next expression:") 44 | col.label(text=build_expression(props)) 45 | 46 | 47 | classes = [ 48 | AUDVIS_PT_spreaddriversNpanel, 49 | ] 50 | -------------------------------------------------------------------------------- /ui/ui_lib.py: -------------------------------------------------------------------------------- 1 | def generators_ui_sequence(self, context, props): 2 | col = self.layout.column(align=True) 3 | col.prop(props, "channel") 4 | if context.scene.sequence_editor: 5 | col.prop_search(props, "sound_sequence", context.scene.sequence_editor, "sequences_all", 6 | icon='SOUND') 7 | if props.sound_sequence in context.scene.sequence_editor.sequences_all: 8 | sequence = context.scene.sequence_editor.sequences_all[props.sound_sequence] 9 | if sequence.type != 'SOUND': 10 | row = col.row() 11 | row.alert = True 12 | row.label(text="Selected Sequence is not a sound") 13 | col.prop(props, "sequence_channel") 14 | 15 | 16 | def generators_ui_midi(self, context, midi_props): 17 | box = self.layout.box().column(align=True) 18 | box.label(text="MIDI") 19 | box.prop_search(midi_props, "device", 20 | context.scene.audvis.midi_realtime, "inputs", 21 | text="Device", 22 | icon='PLAY_SOUND') 23 | box.prop_search(midi_props, "file", 24 | context.scene.audvis.midi_file, "midi_files", 25 | text="File") 26 | if midi_props.file in context.scene.audvis.midi_file.midi_files: 27 | midifile = context.scene.audvis.midi_file.midi_files[midi_props.file] 28 | 29 | box.prop_search(midi_props, "track", 30 | midifile, "tracks", 31 | text="Track") 32 | else: 33 | box.prop(midi_props, "track", text="Track") 34 | box.prop(midi_props, "channel", text="Channel") -------------------------------------------------------------------------------- /ui/values/__init__.py: -------------------------------------------------------------------------------- 1 | from . import presets 2 | from ..buttonspanel import AudVisButtonsPanel_Npanel 3 | 4 | 5 | def get_sample_calc(self): 6 | v = self.sample / 1000 7 | if self.value_aud.enable and self.value_aud.adsr_enable: 8 | v = max([ 9 | v, 10 | self.value_aud.adsr_attack, 11 | self.value_aud.adsr_sustain, 12 | self.value_aud.adsr_release, 13 | ]) 14 | return v 15 | 16 | 17 | class AUDVIS_PT_valuesNpanel(AudVisButtonsPanel_Npanel): 18 | bl_label = "Driver Values" 19 | 20 | @classmethod 21 | def poll(cls, context): 22 | return True 23 | 24 | def draw_header_preset(self, _context): 25 | self.layout.operator("audvis.drivervalues_preset_menu", icon="PRESET", text="") 26 | 27 | def draw(self, context): 28 | self._aud_props(context) 29 | props = context.scene.audvis 30 | layout = self.layout 31 | if not props.value_aud.enable: 32 | col = layout.column(align=True) 33 | col.prop(props, "value_highpass_freq") 34 | col.prop(props, "value_highpass_factor") 35 | col = layout.column(align=True) 36 | # col.prop(props, "value_logarithm") 37 | col.prop(props, "value_factor") 38 | col.prop(props, "value_max") 39 | col.prop(props, "value_add") 40 | col.prop(props, "value_noise") 41 | 42 | col = layout.column(align=True) 43 | row = col.row() 44 | row.label(text="Normalize Value") 45 | row.prop(props, "value_normalize", text="") 46 | if props.value_normalize == 'max': 47 | col.prop(props, "value_normalize_clamp_to") 48 | 49 | col = layout.column(align=True) 50 | row = col.row() 51 | row.label(text="Fade Out Type") 52 | row.prop(props, "value_fadeout_type", text="") 53 | if props.value_fadeout_type != 'off': 54 | col.prop(props, "value_fadeout_speed") 55 | row = col.row() 56 | row.label(text="Additive Type") 57 | row.prop(props, "value_additive_type", text="") 58 | if props.value_additive_type != 'off': 59 | col.prop(props, "value_additive_reset") 60 | if props.value_additive_type != 'off' or props.value_fadeout_type != 'off': 61 | col.label(text="Warning: Fadeout (a little bit)") 62 | col.label(text="and Additive (a lot)") 63 | col.label(text="can cause inconsistent state") 64 | col.label(text="if you skip frames.") 65 | col.label(text="Maybe you want to bake drivers") 66 | col.label(text="before rendering.") 67 | 68 | def _aud_props(self, context): 69 | col = self.layout.column(align=True) 70 | props = context.scene.audvis.value_aud 71 | col.prop(props, "enable") 72 | if props.enable: 73 | col = col.box().column(align=True) 74 | col.prop(props, "highpass") 75 | col.prop(props, "highpass_factor") 76 | col.prop(props, "lowpass") 77 | col.prop(props, "lowpass_factor") 78 | col.prop(props, "use_fake_curve_light") 79 | if props.use_fake_curve_light and props.fake_curve_light is not None: 80 | col.template_curve_mapping(props.fake_curve_light, 'falloff_curve') 81 | 82 | # col.prop(props, "env_enable") 83 | # if props.env_enable: 84 | # col.prop(props, "env_attack") 85 | # col.prop(props, "env_release") 86 | # col.prop(props, "env_threshold") 87 | # col.prop(props, "env_arthreshold") 88 | 89 | col.prop(props, "adsr_enable") 90 | if props.adsr_enable: 91 | col.prop(props, "adsr_attack") 92 | col.prop(props, "adsr_decay") 93 | col.prop(props, "adsr_sustain") 94 | col.prop(props, "adsr_release") 95 | 96 | 97 | classes = presets.classes + [ 98 | AUDVIS_PT_valuesNpanel, 99 | ] 100 | -------------------------------------------------------------------------------- /ui/values/presets.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.types import (Operator, Menu) 3 | 4 | 5 | class _presets: 6 | def _props(self): 7 | return self._scene().audvis 8 | 9 | def _scene(self): 10 | return bpy.context.scene 11 | 12 | def _common(self): 13 | props = self._props() 14 | props.factor = 1 15 | props.max = 100 16 | props.add = 0 17 | props.noise = 0 18 | 19 | def default(self): 20 | self._common() 21 | props = self._props() 22 | props.value_aud.enable = False 23 | props.value_highpass_freq = 1000 24 | props.value_highpass_factor = 0 25 | 26 | def smooth(self): 27 | props = self._props() 28 | props.value_aud.enable = True 29 | props.value_aud.adsr_attack = .2 30 | props.value_aud.adsr_decay = .2 31 | props.value_aud.adsr_sustain = .2 32 | props.value_aud.adsr_release = .2 33 | self._common() 34 | 35 | def highpass_on(self): 36 | props = self._props() 37 | props.value_aud.highpass = 300 38 | props.value_aud.highpass_factor = .5 39 | props.value_highpass_freq = 1000 40 | props.value_highpass_factor = 1 41 | 42 | def highpass_off(self): 43 | props = self._props() 44 | props.value_aud.highpass = 0 45 | props.value_aud.highpass_factor = .5 46 | props.value_highpass_freq = 1000 47 | props.value_highpass_factor = 0 48 | 49 | 50 | class AUDVIS_OT_valuespresetmenu(Operator): 51 | bl_label = "Driver Values Presets" 52 | bl_idname = "audvis.drivervalues_preset_menu" 53 | 54 | def execute(self, context): 55 | bpy.ops.wm.call_menu(name="AUDVIS_MT_valuespreset") 56 | return {'FINISHED'} 57 | 58 | 59 | class AUDVIS_OT_valuespreset(Operator): 60 | bl_label = "Driver Values Presets" 61 | bl_idname = "audvis.drivervalues_preset_exec" 62 | 63 | preset_name: bpy.props.StringProperty(default="") 64 | 65 | def execute(self, context): 66 | obj = _presets() 67 | getattr(obj, self.preset_name)() 68 | return {'FINISHED'} 69 | 70 | 71 | class AUDVIS_MT_valuespreset(Menu): 72 | bl_idname = "AUDVIS_MT_valuespreset" 73 | bl_label = "Driver Values Presets" 74 | 75 | def draw(self, _context): 76 | for preset_name in dir(_presets): 77 | if not preset_name.startswith("_"): 78 | self.layout.operator("audvis.drivervalues_preset_exec", text=preset_name.replace("_", " ").title()).preset_name = preset_name 79 | 80 | def draw_mainmenu(self, _context): # static 81 | self.layout.menu("AUDVIS_MT_valuespreset") 82 | 83 | 84 | classes = [ 85 | AUDVIS_MT_valuespreset, 86 | AUDVIS_OT_valuespreset, 87 | AUDVIS_OT_valuespresetmenu, 88 | ] 89 | -------------------------------------------------------------------------------- /ui/video/__init__.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | from ..buttonspanel import AudVisButtonsPanel_Npanel 4 | 5 | 6 | class AUDVIS_PT_videoNpanel(AudVisButtonsPanel_Npanel): 7 | bl_label = "Video Capture" 8 | 9 | @classmethod 10 | def poll(cls, context): 11 | return True 12 | 13 | def draw_header(self, context): 14 | self.layout.prop(context.scene.audvis, "video_webcam_enable", text="") 15 | 16 | def draw(self, context): 17 | col = self.layout.column(align=True) 18 | # col.enabled = context.scene.audvis.realtime_enable 19 | supported = bpy.audvis.is_video_supported() 20 | if supported: 21 | col.prop(context.scene.audvis, "video_webcam_index") 22 | if context.scene.audvis.video_webcam_enable: 23 | col.prop(context.scene.audvis, "video_image") 24 | col.prop(context.scene.audvis, "video_tmppath") 25 | col.prop(context.scene.audvis, "video_image_flip_horizontal") 26 | col = self.layout.column(align=True) 27 | col.prop(context.scene.audvis, "video_resize") 28 | if context.scene.audvis.video_resize: 29 | col.prop(context.scene.audvis, "video_resize_relative") 30 | if context.scene.audvis.video_resize_relative: 31 | col.prop(context.scene.audvis, "video_resize_ratio") 32 | else: 33 | col.prop(context.scene.audvis, "video_width") 34 | col.prop(context.scene.audvis, "video_height") 35 | self._draw_contour(context) 36 | else: 37 | col.label(text="Video not supported. Install opencv first:") 38 | col.operator("audvis.install", text="Install python packages") 39 | err = bpy.audvis.get_video_error() 40 | if err: 41 | col = self.layout.column(align=True) 42 | col.label(text="Error: " + err) 43 | 44 | def _draw_contour(self, context): 45 | col = self.layout.column(align=True) 46 | col.prop(context.scene.audvis, "video_contour_enable") 47 | if context.scene.audvis.video_contour_enable: 48 | # col.prop(context.scene.audvis, "video_contour_object_type") # TODO: curve and mesh are useless for now... 49 | col.prop(context.scene.audvis, "video_contour_chain_approx") 50 | col.prop(context.scene.audvis, "video_contour_threshold") 51 | col.prop(context.scene.audvis, "video_contour_simplify") 52 | col.prop(context.scene.audvis, "video_contour_min_points_per_stroke") 53 | col.prop(context.scene.audvis, "video_contour_size") 54 | 55 | 56 | def on_blendfile_loaded(): 57 | if hasattr(bpy.context, "scene"): 58 | img = bpy.context.scene.audvis.video_image 59 | if img is not None and hasattr(img, "pixels") and len(img.pixels) == 0: 60 | img.source = 'GENERATED' 61 | img.scale(100, 100) 62 | 63 | 64 | def on_blendfile_save(): 65 | pass 66 | 67 | 68 | def unregister(): 69 | try: 70 | bpy.audvis.video_analyzer.kill() 71 | bpy.audvis.video_analyzer = None 72 | except: 73 | pass 74 | 75 | 76 | classes = [ 77 | AUDVIS_PT_videoNpanel, 78 | ] 79 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | MidiNoteMap = { 4 | 'A0': 21, 5 | 'A#0': 22, 6 | 'Bb0': 22, 7 | 'B0': 23, 8 | } 9 | for octave in [1, 2, 3, 4, 5, 6, 7, 8, 9]: 10 | for i, (note, offset) in enumerate({ 11 | 'C': 0, 12 | 'C#': 1, 'Db': 1, 13 | 'D': 2, 14 | 'D#': 3, 'Eb': 3, 15 | 'E': 4, 16 | 'F': 5, 17 | 'F#': 6, 'Gb': 6, 18 | 'G': 7, 19 | 'G#': 8, 'Ab': 8, 20 | 'A': 9, 21 | 'A#': 10, 'Bb': 10, 22 | 'B': 11, 23 | }.items()): 24 | MidiNoteMap[note + str(octave)] = 24 + ((octave - 1) * 12) + offset 25 | if note == 'Ab' and octave == 9: 26 | break 27 | 28 | 29 | def midi_note_to_number(value): 30 | if type(value) is str: 31 | value = value.capitalize() 32 | if value in MidiNoteMap: 33 | return MidiNoteMap[value] # 'C4' => 60 34 | return int(value) # '60' => 60 35 | return value # 60 => 60 36 | 37 | 38 | def midi_number_to_note(value): 39 | for (note_name, midi_number) in list(MidiNoteMap.items()): 40 | if value == midi_number: 41 | return note_name 42 | return None 43 | 44 | 45 | def call_ops_override(operator, override, **kwargs): 46 | if hasattr(bpy.context, 'temp_override'): # blender 3.2 and higher 47 | with bpy.context.temp_override(**override): 48 | operator(**kwargs) 49 | else: 50 | operator(override, **kwargs) 51 | --------------------------------------------------------------------------------