diff --git a/ACTIONS.md b/ACTIONS.md index 260c9e8..38f368a 100644 --- a/ACTIONS.md +++ b/ACTIONS.md @@ -1,8 +1,8 @@ # Auto-generated Actions list -Fri 3 Jan 22:08:36 UTC 2020 +Fri 10 Jan 17:25:14 UTC 2020 -for branch=feature_shader_midi +for branch=feature_plugins # Methods * change_composite_setting(setting_value) @@ -58,6 +58,7 @@ for branch=feature_shader_midi * load_this_detour_shader * _load_this_slot_into_next_player(slot) * map_on_shaders_selection + * modulate_param_layer_offset_to_amount(param, layer, amount) * move_browser_selection_down * move_browser_selection_up * move_settings_selection_down @@ -156,7 +157,7 @@ for branch=feature_shader_midi * toggle_x_autorepeat * try_pull_code_and_reset -# Dynamic routes +## Dynamic routes * play_shader_([0-9])_([0-9]) * toggle_shader_layer_([0-2]) * start_shader_layer_([0-2]) @@ -171,6 +172,15 @@ for branch=feature_shader_midi * set_shader_speed_layer_offset_([0-2])_amount * set_shader_speed_layer_([0-2])_amount +### Plugin routes + * test_plugin (from MidiActionsTestPlugin) + * cycle_shaders (from MidiActionsTestPlugin) + * run_automation (from MidiActionsTestPlugin) + * stop_automation (from MidiActionsTestPlugin) + * toggle_pause_automation (from MidiActionsTestPlugin) + * pause_automation (from MidiActionsTestPlugin) + * toggle_loop_automation (from MidiActionsTestPlugin) + ---- Autogenerated by dotfiles/generate-list-actions.sh diff --git a/actions.py b/actions.py index 21a11c2..caf4281 100644 --- a/actions.py +++ b/actions.py @@ -530,7 +530,7 @@ class Actions(object): self.data.settings['shader']['STROBE_AMOUNT']['value'] = scaled_amount def get_midi_status(self): - self.message_handler.set_message('INFO', 'midi status is {}'.format(self.data.midi_status)) + self.message_handler.set_message('INFO', ("midi status is {} to %s"%(self.data.midi_device_name)).format(self.data.midi_status)) def cycle_midi_port_index(self): self.data.midi_port_index = self.data.midi_port_index + 1 @@ -573,13 +573,13 @@ class Actions(object): self.data.update_setting_value('video', 'OUTPUT', 'composite') else: self.data.update_setting_value('video', 'OUTPUT', 'hdmi') - + if self.data.settings['video']['HDMI_MODE']['value'] == "CEA 4 HDMI": self.data.update_setting_value('video', 'HDMI_MODE', 'CEA 4 HDMI') self.change_hdmi_settings('CEA 4 HDMI') - + def check_dev_mode(self): #### check if in dev mode:(ie not using the lcd screen) @@ -591,8 +591,9 @@ class Actions(object): def check_if_should_start_openframeworks(self): if self.data.settings['video']['VIDEOPLAYER_BACKEND']['value'] != 'omxplayer': - self.openframeworks_process = subprocess.Popen([self.data.PATH_TO_OPENFRAMEWORKS +'apps/myApps/c_o_n_j_u_r/bin/c_o_n_j_u_r']) - print('conjur pid is {}'.format(self.openframeworks_process.pid)) + with open("conjur.log","w+") as out: + self.openframeworks_process = subprocess.Popen([self.data.PATH_TO_OPENFRAMEWORKS +'apps/myApps/c_o_n_j_u_r/bin/c_o_n_j_u_r'], stdout=out) + print('conjur pid is {}'.format(self.openframeworks_process.pid)) def exit_openframeworks(self): self.video_driver.osc_client.send_message("/exit", True) @@ -736,13 +737,13 @@ class Actions(object): options = self.data.settings['shader']['SHADER_PARAM']['options'] current_index = [index for index, item in enumerate(options) if item == self.data.settings['shader']['SHADER_PARAM']['value'] ][0] self.data.settings['shader']['SHADER_PARAM']['value'] = options[(current_index + 1) % len(options) ] - self.message_handler.set_message('INFO', 'The Param amountis now ' + str(self.data.settings['shader']['SHADER_PARAM']['value'])) + self.message_handler.set_message('INFO', 'The Param amount is now ' + str(self.data.settings['shader']['SHADER_PARAM']['value'])) def decrease_shader_param(self): options = self.data.settings['shader']['SHADER_PARAM']['options'] current_index = [index for index, item in enumerate(options) if item == self.data.settings['shader']['SHADER_PARAM']['value'] ][0] self.data.settings['shader']['SHADER_PARAM']['value'] = options[(current_index - 1) % len(options) ] - self.message_handler.set_message('INFO', 'The Param amountis now ' + str(self.data.settings['shader']['SHADER_PARAM']['value'])) + self.message_handler.set_message('INFO', 'The Param amount is now ' + str(self.data.settings['shader']['SHADER_PARAM']['value'])) def set_fixed_length(self, value): @@ -893,6 +894,9 @@ class Actions(object): def clear_message(self): self.message_handler.clear_all_messages() + def modulate_param_layer_offset_to_amount(self, param, layer, amount): + self.shaders.modulate_param_layer_offset_to_amount(param, amount, layer_offset=layer) + @staticmethod def try_remove_file(path): if os.path.exists(path): @@ -902,12 +906,12 @@ class Actions(object): # this would include eg a custom script module.. @property def parserlist(self): - return { + return { ( r"play_shader_([0-9])_([0-9])", self.shaders.play_that_shader ), ( r"toggle_shader_layer_([0-2])", self.toggle_shader_layer ), ( r"start_shader_layer_([0-2])", self.shaders.start_shader ), ( r"stop_shader_layer_([0-2])", self.shaders.stop_shader ), - ( r"set_the_shader_param_([0-3])_layer_([0-2])_continuous", self.shaders.set_param_layer_to_amount ), + ( r"set_the_shader_param_([0-3])_layer_([0-2])_continuous", self.shaders.set_param_layer_to_amount ), ( r"modulate_param_([0-3])_to_amount_continuous", self.shaders.modulate_param_to_amount ), ( r"set_param_([0-3])_layer_([0-2])_modulation_level_continuous", self.shaders.set_param_layer_offset_modulation_level ), ( r"set_param_([0-3])_layer_offset_([0-2])_modulation_level_continuous", self.shaders.set_param_layer_offset_modulation_level ), @@ -915,7 +919,7 @@ class Actions(object): ( r"reset_modulation_([0-3])", self.shaders.reset_modulation ), ( r"select_shader_modulation_slot_([0-3])", self.shaders.select_shader_modulation_slot ), ( r"set_shader_speed_layer_offset_([0-2])_amount", self.shaders.set_speed_offset_to_amount ), - ( r"set_shader_speed_layer_([0-2])_amount", self.shaders.set_speed_layer_to_amount ), + ( r"set_shader_speed_layer_([0-2])_amount", self.shaders.set_speed_layer_to_amount ), } def detect_types(self, args): @@ -951,6 +955,16 @@ class Actions(object): def call_parse_method_name(self, method_name, argument): + # first test if a registered plugin handles this for us + from data_centre.plugin_collection import ActionsPlugin + for plugin in self.data.plugins.get_plugins(ActionsPlugin): + if plugin.is_handled(method_name): + print ("Plugin %s is handling %s" % (plugin, method_name)) + method, arguments = plugin.get_callback_for_method(method_name, argument) + method(*arguments) + return + + # if not then fall back to using internal method try: method, arguments = self.get_callback_for_method(method_name, argument) method(*arguments) diff --git a/data_centre/data.py b/data_centre/data.py index 08ffddf..ddf4eb6 100644 --- a/data_centre/data.py +++ b/data_centre/data.py @@ -7,6 +7,24 @@ import inspect from itertools import cycle from omxplayer.player import OMXPlayer from shutil import copyfile +import threading + +from data_centre import plugin_collection + +class AsyncWrite(threading.Thread): + def __init__(self, filename, data, mode='json'): + threading.Thread.__init__(self) + self.filename = filename + self.data = data + self.mode = mode + + def run(self): + with open(self.filename, "w+") as data_file: + if self.mode=='json': + json.dump(self.data, data_file, indent=4, sort_keys=True) + else: + data_file.write(self.data) + data_file.close() @@ -33,6 +51,11 @@ class Data(object): #self.EMPTY_BANK = [self.EMPTY_SLOT for i in range(10)] self.PATHS_TO_BROWSER = [self.PATH_TO_EXTERNAL_DEVICES, '/home/pi/Videos' ] self.PATHS_TO_SHADERS = [self.PATH_TO_EXTERNAL_DEVICES, '/home/pi/r_e_c_u_r/Shaders', '/home/pi/Shaders' ] + self.PATHS_TO_PLUGIN_DATA = [ '/home/pi/r_e_c_u_r/json_objects/plugins', self.PATH_TO_EXTERNAL_DEVICES ] + + #initialise plugin manager + self.plugins = plugin_collection.PluginCollection("plugins", message_handler, self) + self.plugins.apply_all_plugins_on_value(5) ### state data self.auto_repeat_on = True @@ -78,7 +101,17 @@ class Data(object): self.midi_mappings = self._read_json(self.MIDI_MAPPING_JSON) self.analog_mappings = self._read_json(self.ANALOG_MAPPING_JSON) - + def load_midi_mapping_for_device(self, device_name): + # check if custom config file exists on disk for this device name + custom_file = self.MIDI_MAPPING_JSON.replace(".json","_%s.json"%device_name) + if os.path.isfile(self.PATH_TO_DATA_OBJECTS + custom_file): + self.midi_mappings = self._read_json(custom_file) + self.message_handler.set_message('INFO', "Loaded %s for %s" % (custom_file, device_name)) #the slot you pressed is empty') + print ("loaded custom midi mapping for %s" % custom_file) + else: + print ("loading default midi mapping for %s" % (device_name)) + self.midi_mappings = self._read_json(self.MIDI_MAPPINGS_JSON) + return self.midi_mappings @staticmethod @@ -100,6 +133,23 @@ class Data(object): with open('{}{}'.format(self.PATH_TO_DATA_OBJECTS, file_name), 'w') as data_file: json.dump(data, data_file, indent=4, sort_keys=True) + def _read_plugin_json(self, file_name): + for path in self.PATHS_TO_PLUGIN_DATA: + print("trying path %s"%path) + try: + with open("%s/%s" % (path,file_name)) as data_file: + data = json.load(data_file) + return data + except: + pass + print ("no plugin data loaded for %s" % file_name) + + def _update_plugin_json(self, file_name, data): + #with open("%s/%s" % (self.PATHS_TO_PLUGIN_DATA[0], file_name), "w+") as data_file: + # json.dump(data, data_file, indent=4, sort_keys=True) + writer = AsyncWrite("%s/%s" % (self.PATHS_TO_PLUGIN_DATA[0], file_name), data, mode='json') + writer.start() + def update_conjur_dev_mode(self, value): print(value) tree = ET.parse(self.PATH_TO_CONJUR_DATA) diff --git a/data_centre/plugin_collection.py b/data_centre/plugin_collection.py new file mode 100644 index 0000000..ba9f4cc --- /dev/null +++ b/data_centre/plugin_collection.py @@ -0,0 +1,270 @@ +import inspect +import os +import pkgutil +import re + + +class Plugin(object): + """Base class that each plugin must inherit from. within this class + you must define the methods that all of your plugins must implement + """ + disabled = False + + def __init__(self, plugin_collection): + self.description = 'UNKNOWN' + self.pc = plugin_collection + +class MidiFeedbackPlugin(Plugin): + """Base class for MIDI feedback plugins + """ + def __init__(self, plugin_collection): + super().__init__(plugin_collection) + self.description = 'Outputs feedback about status to device eg MIDI pads' + + def supports_midi_feedback(self, device_name): + return False + + def set_midi_device(self, midi_device): + self.midi_feedback_device = midi_device + + def refresh_midi_feedback(self): + raise NotImplementedError + + + +class SequencePlugin(Plugin): + def __init__(self, plugin_collection): + super().__init__(plugin_collection) + + @property + def parserlist(self): + return [ + ( r"run_automation", self.run_automation ), + ( r"stop_automation", self.stop_automation ), + ( r"toggle_pause_automation", self.toggle_pause_automation ), + ( r"pause_automation", self.pause_automation ), + ( r"toggle_loop_automation", self.toggle_loop_automation ), + ] + + def position(self, now): + import time + passed = now - self.automation_start + if self.duration>0: + position = passed / self.duration*1000 + return position + + def toggle_automation(self): + if not self.is_playing(): + self.run_automation() + else: + self.stop_automation() + + def toggle_loop_automation(self): + self.looping = not self.looping + + def pause_automation(self): + self.pause_flag = not self.is_paused() and self.is_playing() + + def stop_automation(self): + self.stop_flag = True + + def toggle_pause_automation(self): + self.pause_flag = not self.is_paused() + self.pause_flag = self.is_paused() and self.is_playing() + if not self.is_paused() and not self.is_playing(): + self.run_automation() + + store_passed = None + pause_flag = True + stop_flag = False + looping = True + automation_start = None + iterations_count = 0 + duration = 2000 + frequency = 100 + def run_automation(self): + import time + + now = time.time() + + if self.looping and self.automation_start is not None and (now - self.automation_start >= self.duration/1000): + print("restarting as start reached %s" % self.automation_start) + self.iterations_count += 1 + self.automation_start = None + + if not self.automation_start: + self.automation_start = now + print ("%s: starting automation" % self.automation_start) + self.pause_flag = False + + #print("running automation at %s!" % self.position) + if not self.is_paused(): + self.store_passed = None + self.run_sequence(self.position(now)) + #print ("%s: automation_start is %s" % (time.time()-self.automation_start,self.automation_start)) + else: + #print ("%s: about to reset automation_start" % self.automation_start) + #print (" got passed %s" % (time.time() - self.automation_start)) + if not self.store_passed: + self.store_passed = (now - self.automation_start) + self.automation_start = now - self.store_passed + #print ("%s: reset automation_start to %s" % (time.time()-self.automation_start,self.automation_start)) + #return + + if (now - self.automation_start < self.duration/1000) and not self.stop_flag: + self.pc.midi_input.root.after(self.frequency, self.run_automation) + else: + print("%s: stopping ! (stop_flag %s)" % ((now - self.automation_start),self.stop_flag) ) + self.stop_flag = False + self.automation_start = None + self.iterations_count = 0 + + def is_paused(self): + return self.pause_flag + + def is_playing(self): + return self.automation_start is not None + + def run_sequence(self, position): + raise NotImplementedError + + +class ActionsPlugin(Plugin): + def __init__(self, plugin_collection): + super().__init__(plugin_collection) + + @property + def parserlist(self): + return [ + #( r"test_plugin", self.test_plugin ) + ] + + def is_handled(self, method_name): + for a in self.parserlist: + if (a[0]==method_name): + return True + regex = a[0] + me = a[1] + matches = re.match(regex, method_name) + if matches: + return True + + + def get_callback_for_method(self, method_name, argument): + for a in self.parserlist: + regex = a[0] + me = a[1] + matches = re.search(regex, method_name) + + if matches: + found_method = me + parsed_args = self.pc.actions.detect_types(matches.groups()) + if argument is not None: + args = parsed_args + [argument] + else: + args = parsed_args + + return (found_method, args) + + #def call_parse_method_name(self, method_name, argument): + # method, arguments = self.actions.get_callback_for_method(method_name, argument) + # method(*arguments) + + +# adapted from https://github.com/gdiepen/python_plugin_example +class PluginCollection(object): + """Upon creation, this class will read the plugins package for modules + that contain a class definition that is inheriting from the Plugin class + """ + + @property + def shaders(self): + return self.message_handler.shaders + + @property + def actions(self): + return self.message_handler.actions + + """@property + def midi_input(self): + return self.data.midi_input""" + + def __init__(self, plugin_package, message_handler, data): + """Constructor that initiates the reading of all available plugins + when an instance of the PluginCollection object is created + """ + self.plugin_package = plugin_package + self.message_handler = message_handler + #self.shaders = lambda: data.shaders + self.data = data + #self.actions = message_handler.actions + self.reload_plugins() + + def read_json(self, file_name): + return self.data._read_plugin_json(file_name) + def update_json(self, file_name, data): + return self.data._update_plugin_json(file_name, data) + + def reload_plugins(self): + """Reset the list of all plugins and initiate the walk over the main + provided plugin package to load all available plugins + """ + self.plugins = [] + self.seen_paths = [] + print() + print("Looking for plugins under package %s" % self.plugin_package) + self.walk_package(self.plugin_package) + + + def get_plugins(self, clazz = None): + if clazz: + return [c for c in self.plugins if (isinstance(c, clazz) and not c.disabled)] + else: + return [c for c in self.plugins if not c.disabled] + + def apply_all_plugins_on_value(self, argument): + """Apply all of the plugins on the argument supplied to this function + """ + print() + print('Applying all plugins on value %s:' %argument) + for plugin in self.plugins: + #print(" Applying %s on value %s yields value %s" % (plugin.description, argument, plugin.perform_operation(argument))) + pass + + + def walk_package(self, package): + """Recursively walk the supplied package to retrieve all plugins + """ + imported_package = __import__(package, fromlist=['blah']) + + for _, pluginname, ispkg in pkgutil.iter_modules(imported_package.__path__, imported_package.__name__ + '.'): + if not ispkg: + plugin_module = __import__(pluginname, fromlist=['blah']) + clsmembers = inspect.getmembers(plugin_module, inspect.isclass) + for (_, c) in clsmembers: + # Only add classes that are a sub class of Plugin, but NOT Plugin itself + # or one of the base classes + ignore_list = [ Plugin, ActionsPlugin, SequencePlugin, MidiFeedbackPlugin ] + if issubclass(c, Plugin) & (c not in ignore_list): + print(' Found plugin class: %s.%s' % (c.__module__,c.__name__)) + self.plugins.append(c(self)) + + + # Now that we have looked at all the modules in the current package, start looking + # recursively for additional modules in sub packages + all_current_paths = [] + if isinstance(imported_package.__path__, str): + all_current_paths.append(imported_package.__path__) + else: + all_current_paths.extend([x for x in imported_package.__path__]) + + for pkg_path in all_current_paths: + if pkg_path not in self.seen_paths: + self.seen_paths.append(pkg_path) + + # Get all sub directory of the current package path directory + child_pkgs = [p for p in os.listdir(pkg_path) if os.path.isdir(os.path.join(pkg_path, p))] + + # For each sub directory, apply the walk_package method recursively + for child_pkg in child_pkgs: + self.walk_package(package + '.' + child_pkg) diff --git a/dotfiles/generate-list-actions.sh b/dotfiles/generate-list-actions.sh index dc555cb..e5afe73 100755 --- a/dotfiles/generate-list-actions.sh +++ b/dotfiles/generate-list-actions.sh @@ -11,8 +11,12 @@ grep " def " actions.py | grep -v "^#" | sed -e 's/ def //' | sed -e 's/self//' | grep -v "parserlist\|check_if_should_start_openframeworks\|create_serial_port_process\|__init__\|persist_composite_setting\|receive_detour_info\|_refresh_frame_buffer\|refresh_frame_buffer_and_restart_openframeworks\|run_script\|setup_osc_server\|start_confirm_action\|stop_serial_port_process\|stop_openframeworks_process\|update_capture_settings\|update_config_settings\|update_video_settings\|try_remove_file\|get_callback\|call_method_name\|call_parse_method\|detect_types" echo -echo "# Dynamic routes" +echo "## Dynamic routes" grep '( r"' actions.py | sed -e 's/\(.*\)"\(.*\)"\(.*\)/ * \2/' +echo + +echo "### Plugin routes" +grep "( r\"" plugins/*.py | sed -e 's/plugins\/\(.*\)\.py:\(.*\)\( r\"\)\(.*\)\"\(.*\)/ * \4\t(from \1)/' | grep -v "open_serial" echo echo "----" diff --git a/json_objects/midi_action_mapping.json b/json_objects/midi_action_mapping.json index c2eae1b..2ad64c8 100644 --- a/json_objects/midi_action_mapping.json +++ b/json_objects/midi_action_mapping.json @@ -1,6 +1,6 @@ { "control_change 0": { - "DEFAULT": ["set_the_shader_param_0_layer_offset_0_continuous"], + "DEFAULT": ["set_the_shader_param_0_layer_offset_0_continuous","set_strobe_amount_continuous"], "NAV_DETOUR": ["set_detour_mix_continuous"] }, "control_change 1": { diff --git a/json_objects/midi_action_mapping_APC Key 25.json b/json_objects/midi_action_mapping_APC Key 25.json new file mode 100644 index 0000000..8fd98ff --- /dev/null +++ b/json_objects/midi_action_mapping_APC Key 25.json @@ -0,0 +1,266 @@ +{ + "control_change 0": { + "DEFAULT": ["set_the_shader_param_0_layer_offset_0_continuous"], + "NAV_DETOUR": ["set_detour_mix_continuous"] + }, + "control_change 1": { + "DEFAULT": ["set_the_shader_param_1_layer_offset_0_continuous"], + "NAV_DETOUR": ["set_detour_speed_position_continuous"] + }, + "control_change 2": { + "DEFAULT": ["set_the_shader_param_2_layer_offset_0_continuous"], + "NAV_DETOUR": ["set_detour_start_continuous"] + }, + "control_change 3": { + "DEFAULT": ["set_the_shader_param_3_layer_offset_0_continuous"], + "NAV_DETOUR": ["set_detour_end_continuous"] + }, + "control_change 4": { + "DEFAULT": ["set_the_shader_param_0_layer_offset_1_continuous"] + }, + "control_change 5": { + "DEFAULT": ["set_the_shader_param_1_layer_offset_1_continuous"] + }, + "control_change 6": { + "DEFAULT": ["set_the_shader_param_2_layer_offset_1_continuous"] + }, + "control_change 7": { + "DEFAULT": ["set_the_shader_param_3_layer_offset_1_continuous"] + }, + "control_change 8": { + "DEFAULT": ["set_the_shader_param_0_layer_offset_2_continuous"] + }, + "control_change 9": { + "DEFAULT": ["set_the_shader_param_1_layer_offset_2_continuous"] + }, + "control_change 10": { + "DEFAULT": ["set_the_shader_param_2_layer_offset_2_continuous"] + }, + "control_change 11": { + "DEFAULT": ["set_the_shader_param_3_layer_offset_2_continuous"] + }, + "control_change 48": { + "DEFAULT": ["set_the_shader_param_0_layer_offset_0_continuous","set_strobe_amount_continuous"], + "NAV_DETOUR": ["set_detour_speed_position_continuous"] + }, + "control_change 49": { + "DEFAULT": ["set_the_shader_param_1_layer_offset_0_continuous","set_shader_speed_layer_0_amount"], + "NAV_DETOUR": ["set_detour_start_continuous"] + }, + "control_change 50": { + "DEFAULT": ["set_the_shader_param_2_layer_offset_0_continuous","set_shader_speed_layer_1_amount"], + "NAV_DETOUR": ["set_detour_end_continuous"] + }, + "control_change 51": { + "DEFAULT": ["set_the_shader_param_3_layer_offset_0_continuous","set_shader_speed_layer_2_amount"], + "NAV_DETOUR": ["set_detour_end_continuous"] + }, + "control_change 52": { + "DEFAULT": ["set_the_shader_param_0_layer_offset_1_continuous","set_param_0_layer_offset_0_modulation_level_continuous"], + "NAV_DETOUR": ["set_detour_speed_position_continuous"] + }, + "control_change 53": { + "DEFAULT": ["set_the_shader_param_1_layer_offset_1_continuous","set_param_1_layer_offset_0_modulation_level_continuous"], + "NAV_DETOUR": ["set_detour_start_continuous"] + }, + "control_change 54": { + "DEFAULT": ["set_the_shader_param_2_layer_offset_1_continuous","set_param_2_layer_offset_0_modulation_level_continuous"], + "NAV_DETOUR": ["set_detour_end_continuous"] + }, + "control_change 55": { + "DEFAULT": ["set_the_shader_param_3_layer_offset_1_continuous","set_param_3_layer_offset_0_modulation_level_continuous"], + "NAV_DETOUR": ["set_detour_end_continuous"] + }, + "control_change 56": { + "DEFAULT": ["set_the_shader_param_0_layer_offset_2_continuous"], + "NAV_DETOUR": ["set_detour_speed_position_continuous"] + }, + "control_change 57": { + "DEFAULT": ["set_the_shader_param_1_layer_offset_2_continuous"], + "NAV_DETOUR": ["set_detour_start_continuous"] + }, + "control_change 58": { + "DEFAULT": ["set_the_shader_param_2_layer_offset_2_continuous"], + "NAV_DETOUR": ["set_detour_end_continuous"] + }, + "control_change 59": { + "DEFAULT": ["set_the_shader_param_3_layer_offset_2_continuous"], + "NAV_DETOUR": ["set_detour_end_continuous"] + }, + "note_on 64": { + "DEFAULT": ["clear_automation"] + }, + "note_on 65": { + "DEFAULT": ["toggle_pause_automation"] + }, + "note_on 66": { + "DEFAULT": ["toggle_record_automation"] + }, + "note_on 67": { + "DEFAULT": ["toggle_overdub_automation"] + }, + "note_on 68": { + "DEFAULT": ["store_next_preset"] + }, + "note_on 69": { + "DEFAULT": ["store_current_preset","clear_current_preset"] + }, + "note_on 0": { + "DEFAULT": ["switch_to_preset_0","select_preset_0"] + }, + "note_on 1": { + "DEFAULT": ["switch_to_preset_1","select_preset_1"] + }, + "note_on 2": { + "DEFAULT": ["switch_to_preset_2","select_preset_2"] + }, + "note_on 3": { + "DEFAULT": ["switch_to_preset_3","select_preset_3"] + }, + "note_on 4": { + "DEFAULT": ["switch_to_preset_4","select_preset_4"] + }, + "note_on 5": { + "DEFAULT": ["switch_to_preset_5","select_preset_5"] + }, + "note_on 6": { + "DEFAULT": ["switch_to_preset_6","select_preset_6"] + }, + "note_on 7": { + "DEFAULT": ["switch_to_preset_7","select_preset_7"] + }, + "note_on 81": { + "DEFAULT": ["","reset_selected_modulation"] + }, + "note_on 82": { + "DEFAULT": ["toggle_shader_layer_0","select_shader_modulation_slot_0"] + }, + "note_on 83": { + "DEFAULT": ["toggle_shader_layer_1","select_shader_modulation_slot_1"] + }, + "note_on 84": { + "DEFAULT": ["toggle_shader_layer_2","select_shader_modulation_slot_2"] + }, + "note_on 85": { + "DEFAULT": ["toggle_feedback","select_shader_modulation_slot_3"] + }, + "note_on 86": { + "DEFAULT": ["toggle_capture_preview"] + }, + "note_on 70": { + "DEFAULT": ["previous_shader_layer"] + }, + "note_on 71": { + "DEFAULT": ["next_shader_layer"] + }, + "note_on 8": { + "DEFAULT": ["toggle_automation_clip_0","select_automation_clip_0"] + }, + "note_on 9": { + "DEFAULT": ["toggle_automation_clip_1","select_automation_clip_1"] + }, + "note_on 10": { + "DEFAULT": ["toggle_automation_clip_2","select_automation_clip_2"] + }, + "note_on 11": { + "DEFAULT": ["toggle_automation_clip_3","select_automation_clip_3"] + }, + "note_on 12": { + "DEFAULT": ["toggle_automation_clip_4","select_automation_clip_4"] + }, + "note_on 13": { + "DEFAULT": ["toggle_automation_clip_5","select_automation_clip_5"] + }, + "note_on 14": { + "DEFAULT": ["toggle_automation_clip_6","select_automation_clip_6"] + }, + "note_on 15": { + "DEFAULT": ["toggle_automation_clip_7","select_automation_clip_7"] + }, + + "note_on 32": { + "DEFAULT": ["play_shader_0_0"] + }, + "note_on 33": { + "DEFAULT": ["play_shader_0_1"] + }, + "note_on 34": { + "DEFAULT": ["play_shader_0_2"] + }, + "note_on 35": { + "DEFAULT": ["play_shader_0_3"] + }, + "note_on 36": { + "DEFAULT": ["play_shader_0_4"] + }, + "note_on 37": { + "DEFAULT": ["play_shader_0_5"] + }, + "note_on 38": { + "DEFAULT": ["play_shader_0_6"] + }, + "note_on 39": { + "DEFAULT": ["play_shader_0_7"] + }, + "note_on 24": { + "DEFAULT": ["play_shader_1_0"] + }, + "note_on 25": { + "DEFAULT": ["play_shader_1_1"] + }, + "note_on 26": { + "DEFAULT": ["play_shader_1_2"] + }, + "note_on 27": { + "DEFAULT": ["play_shader_1_3"] + }, + "note_on 28": { + "DEFAULT": ["play_shader_1_4"] + }, + "note_on 29": { + "DEFAULT": ["play_shader_1_5"] + }, + "note_on 30": { + "DEFAULT": ["play_shader_1_6"] + }, + "note_on 31": { + "DEFAULT": ["play_shader_1_7"] + }, + "note_on 16": { + "DEFAULT": ["play_shader_2_0"] + }, + "note_on 17": { + "DEFAULT": ["play_shader_2_1"] + }, + "note_on 18": { + "DEFAULT": ["play_shader_2_2"] + }, + "note_on 19": { + "DEFAULT": ["play_shader_2_3"] + }, + "note_on 20": { + "DEFAULT": ["play_shader_2_4"] + }, + "note_on 21": { + "DEFAULT": ["play_shader_2_5"] + }, + "note_on 22": { + "DEFAULT": ["play_shader_2_6"] + }, + "note_on 23": { + "DEFAULT": ["play_shader_2_7"] + }, + "note_on 98": { + "DEFAULT": ["function_on"] + }, + "note_off 98": { + "DEFAULT": ["function_off"] + }, + "note_on 91": { + "DEFAULT": ["send_serial_macro_0","send_serial_macro_1"] + }, + "note_on 93": { + "DEFAULT": ["send_serial_string_hellO","send_random_settings"] + } +} + diff --git a/plugins/MidiActionsTestPlugin.py b/plugins/MidiActionsTestPlugin.py new file mode 100644 index 0000000..239906a --- /dev/null +++ b/plugins/MidiActionsTestPlugin.py @@ -0,0 +1,51 @@ +import data_centre.plugin_collection +from data_centre.plugin_collection import ActionsPlugin, SequencePlugin + +class MidiActionsTestPlugin(ActionsPlugin,SequencePlugin): + disabled = True + + def __init__(self, plugin_collection): + super().__init__(plugin_collection) + + @property + def parserlist(self): + return [ + ( r"test_plugin", self.test_plugin ), + ( r"cycle_shaders", self.cycle_shaders ), + ( r"run_automation", self.run_automation ), + ( r"stop_automation", self.stop_automation ), + ( r"toggle_pause_automation", self.toggle_pause_automation ), + ( r"pause_automation", self.pause_automation ), + ( r"toggle_loop_automation", self.toggle_loop_automation ), + ] + + def test_plugin(self): + print ("TEST PLUGIN test_plugin CALLED!!") + # can now access various parts of recur via self.pc + + cycle_count = 0 + def cycle_shaders(self): + print ("Cycle shaders!!!") + if self.cycle_count>9: + self.cycle_count = 0 + + for i,shader in enumerate(self.pc.message_handler.shaders.selected_shader_list): + self.pc.midi_input.call_method_name( + "play_shader_%s_%s" % (i, self.cycle_count), None + ) + self.pc.midi_input.call_method_name( + "start_shader_layer_%s" % i, None + ) + self.cycle_count += 1 + + duration = 5000 + frequency = 50 + def run_sequence(self, position): + self.pc.midi_input.call_method_name( + "set_the_shader_param_0_layer_0_continuous", position + ) + + self.pc.midi_input.call_method_name( + "set_the_shader_param_1_layer_0_continuous", position + ) + diff --git a/plugins/MidiFeedbackAPCKey25Plugin.py b/plugins/MidiFeedbackAPCKey25Plugin.py new file mode 100644 index 0000000..cf22f0f --- /dev/null +++ b/plugins/MidiFeedbackAPCKey25Plugin.py @@ -0,0 +1,202 @@ +from data_centre import plugin_collection +from data_centre.plugin_collection import MidiFeedbackPlugin +import mido + +class MidiFeedbackAPCKey25Plugin(MidiFeedbackPlugin): + disabled = False + + status = {} + + def __init__(self, plugin_collection): + super().__init__(plugin_collection) + self.description = 'Outputs feedback to APC Key 25' + + def set_midi_device(self, device): + super().set_midi_device(device) + self.last_state = None + + def supports_midi_feedback(self, device_name): + supported_devices = ['APC Key 25'] + for supported_device in supported_devices: + if device_name.startswith(supported_device): + return True + + def set_status(self, command='note_on', note=None, velocity=None): + self.status[note] = { + 'command': command, + 'note': note, + 'velocity': velocity + } + #print("set status to %s: %s" % (note, self.status[note])) + + def send_command(self, command='note_on', note=None, velocity=None): + #print("send_command(%s, %s)" % (note, velocity)) + self.midi_feedback_device.send( + mido.Message(command, note=note, velocity=velocity) + ) + + def feedback_shader_feedback(self, on): + self.set_status(note=85, velocity=int(on)) + + def feedback_capture_preview(self, on): + self.set_status(note=86, velocity=int(on)) + + def feedback_shader_on(self, layer, slot, colour=None): + if colour is None: colour = self.COLOUR_GREEN + self.set_status(note=(32-(layer)*8)+slot, velocity=int(colour)) + + def feedback_shader_off(self, layer, slot): + self.set_status(note=(32-(layer)*8)+slot, velocity=self.COLOUR_OFF) + + NOTE_SCENE_LAUNCH_COLUMN = 82 + def feedback_shader_layer_on(self, layer): + self.set_status(note=self.NOTE_SCENE_LAUNCH_COLUMN+layer, velocity=self.COLOUR_GREEN) + + def feedback_shader_layer_off(self, layer): + self.set_status(note=self.NOTE_SCENE_LAUNCH_COLUMN+layer, velocity=self.COLOUR_OFF) + + def feedback_show_layer(self, layer): + self.set_status(note=70, velocity=layer) + + def feedback_show_modulation(self, slot): + for i in range(self.NOTE_SCENE_LAUNCH_COLUMN,self.NOTE_SCENE_LAUNCH_COLUMN+4): + if slot==i-self.NOTE_SCENE_LAUNCH_COLUMN: + self.set_status(note=i, velocity=self.COLOUR_GREEN) + else: + self.set_status(note=i, velocity=self.COLOUR_OFF) + + def feedback_plugin_status(self): + from data_centre.plugin_collection import SequencePlugin + + from plugins.MidiActionsTestPlugin import MidiActionsTestPlugin + from plugins.ShaderLoopRecordPlugin import ShaderLoopRecordPlugin + for plugin in self.pc.get_plugins(SequencePlugin): + if isinstance(plugin, ShaderLoopRecordPlugin): #MidiActionsTestPlugin): + + NOTE_PLAY_STATUS = 65 + NOTE_RECORD_STATUS = 66 + NOTE_OVERDUB_STATUS = 67 + NOTE_CLIP_STATUS_ROW = 8 + + colour = self.COLOUR_OFF + if plugin.is_playing(): + colour = self.COLOUR_GREEN + if plugin.is_paused(): + colour += self.BLINK + self.set_status(command='note_on', note=NOTE_PLAY_STATUS, velocity=colour) + + colour = self.COLOUR_OFF + if plugin.recording: + colour = self.COLOUR_GREEN + if plugin.is_ignoring(): + colour += self.BLINK + self.set_status(command='note_on', note=NOTE_RECORD_STATUS, velocity=colour) + + colour = self.COLOUR_OFF + if plugin.overdub: + colour = self.COLOUR_RED + if plugin.is_paused() or plugin.is_ignoring(): + colour += self.BLINK + self.set_status(command='note_on', note=NOTE_OVERDUB_STATUS, velocity=colour) + + for i in range(plugin.MAX_CLIPS): + if i in plugin.running_clips: + if plugin.is_playing() and not plugin.is_paused(): + colour = self.COLOUR_GREEN + else: + colour = self.COLOUR_AMBER + if plugin.selected_clip==i: #blink if selected + colour += self.BLINK + elif plugin.selected_clip==i: + colour = self.COLOUR_RED_BLINK + else: + colour = self.COLOUR_OFF + self.set_status(command='note_on', note=NOTE_CLIP_STATUS_ROW+i, velocity=colour) + + + from plugins.ShaderQuickPresetPlugin import ShaderQuickPresetPlugin + #print ("feedback_plugin_status") + for plugin in self.pc.get_plugins(ShaderQuickPresetPlugin): + #print ("for plugin %s" % plugin) + for pad in range(0,8): + #print ("checking selected_preset %s vs pad %s" % (plugin.selected_preset, pad)) + colour = self.COLOUR_OFF + if plugin.presets[pad] is not None: + colour = self.COLOUR_AMBER + if plugin.last_recalled==pad: + colour = self.COLOUR_GREEN + if plugin.selected_preset==pad: + if plugin.presets[pad] is None: + colour = self.COLOUR_RED + colour += self.BLINK + self.set_status(command='note_on', note=pad, velocity=colour) + + BLINK = 1 + COLOUR_OFF = 0 + COLOUR_GREEN = 1 + COLOUR_GREEN_BLINK = 2 + COLOUR_RED = 3 + COLOUR_RED_BLINK = 4 + COLOUR_AMBER = 5 + COLOUR_AMBER_BLINK = 6 + + def refresh_midi_feedback(self): + + # show which layer is selected (where parameter offset goes to) + self.feedback_show_layer(self.pc.data.shader_layer) + + # show if internal feedback (the shader layer kind) is enabled + if self.pc.data.feedback_active and not self.pc.data.function_on: + self.feedback_shader_feedback(self.COLOUR_GREEN) + #elif self.pc.data.settings['shader']['X3_AS_SPEED']['value'] == 'enabled' and self.pc.data.function_on: + # self.feedback_shader_feedback(self.COLOUR_GREEN_BLINK) + else: + self.feedback_shader_feedback(self.COLOUR_OFF) + + if self.pc.message_handler.actions.display.capture.is_previewing and not self.pc.data.function_on: + self.feedback_capture_preview(self.COLOUR_GREEN) + else: + self.feedback_capture_preview(self.COLOUR_OFF) + + if self.pc.data.function_on: + self.feedback_show_modulation(self.pc.shaders.selected_modulation_slot) + + self.feedback_plugin_status() + + for n,shader in enumerate(self.pc.message_handler.shaders.selected_shader_list): + #print ("%s: in refresh_midi_feedback, got shader: %s" % (n,shader)) + # show if layer is running or not + if not self.pc.data.function_on: + if self.pc.message_handler.shaders.selected_status_list[n] == '▶': + self.feedback_shader_layer_on(n) + else: + self.feedback_shader_layer_off(n) + for x in range(0,8): + if 'slot' in shader and shader.get('slot',None)==x: + if self.pc.message_handler.shaders.selected_status_list[n] == '▶': + # show that slot is selected and running + self.feedback_shader_on(n, x, self.COLOUR_GREEN) + else: + # show that slot is selected but not running + self.feedback_shader_on(n, x, self.COLOUR_AMBER_BLINK) + elif self.pc.data.shader_bank_data[n][x]['path']: + # show that slot is full but not selected + self.feedback_shader_on(n, x, self.COLOUR_AMBER) + else: + # hos that nothing in slot + self.feedback_shader_off(n, x) + + self.update_device() + + #print("refresh_midi_feedback") + + last_state = None + def update_device(self): + from copy import deepcopy + #print("in update device status is %s" % self.status) + for i,c in self.status.items(): + #'print("comparing\n%s to\n%s" % (c, self.last_state[i])) + if self.last_state is None or self.last_state[i]!=c: + #print("got command: %s: %s" % (i,c)) + self.send_command(**c) + self.last_state = deepcopy(self.status) diff --git a/r_e_c_u_r.py b/r_e_c_u_r.py index 09e60ff..2cf0d55 100755 --- a/r_e_c_u_r.py +++ b/r_e_c_u_r.py @@ -52,10 +52,13 @@ display = Display(tk, video_driver, shaders, message_handler, data) # setup the actions actions = Actions(tk, message_handler, data, video_driver, shaders, display, osc_client) +message_handler.actions = actions numpad_input = NumpadInput(tk, message_handler, display, actions, data) osc_input = OscInput(tk, message_handler, display, actions, data) midi_input = MidiInput(tk, message_handler, display, actions, data) +data.plugins.midi_input = midi_input + analog_input = AnalogInput(tk, message_handler, display, actions, data) actions.check_and_set_output_mode_on_boot() diff --git a/video_centre/shaders.py b/video_centre/shaders.py index ffbf838..f182334 100644 --- a/video_centre/shaders.py +++ b/video_centre/shaders.py @@ -9,13 +9,18 @@ class Shaders(object): self.root = root self.osc_client = osc_client self.message_handler = message_handler + self.message_handler.shaders = self self.data = data self.shaders_menu = menu.ShadersMenu(self.data, self.message_handler, self.MENU_HEIGHT ) self.selected_shader_list = [self.EMPTY_SHADER for i in range(3)] self.focused_param = 0 self.shaders_menu_list = self.generate_shaders_list() + + self.selected_modulation_slot = 0 self.selected_status_list = ['-','-','-'] ## going to try using symbols for this : '-' means empty, '▶' means running, '■' means not running, '!' means error + self.selected_modulation_level = [[[0.0,0.0,0.0,0.0] for i in range(4)] for i in range(3)] + self.modulation_value = [0.0,0.0,0.0,0.0] self.selected_param_list = [[0.0,0.0,0.0,0.0] for i in range(3)] self.selected_speed_list = [1.0, 1.0, 1.0] @@ -87,11 +92,13 @@ class Shaders(object): def start_shader(self, layer): self.osc_client.send_message("/shader/{}/is_active".format(str(layer)), True) - self.selected_status_list[layer] = '▶' + if self.selected_status_list[layer] != '-': + self.selected_status_list[layer] = '▶' def stop_shader(self, layer): self.osc_client.send_message("/shader/{}/is_active".format(str(layer)), False) - self.selected_status_list[layer] = '■' + if self.selected_status_list[layer] != '-': + self.selected_status_list[layer] = '■' def start_selected_shader(self): self.start_shader(self.data.shader_layer) @@ -128,9 +135,10 @@ class Shaders(object): def play_that_shader(self, layer, slot): if self.data.shader_bank_data[layer][slot]['path']: - self.selected_shader_list[layer] = self.data.shader_bank_data[layer][slot] - self.selected_shader_list[layer]['slot'] = slot - self.load_shader_layer(layer) + if self.selected_shader_list[layer].get('slot') is None or self.selected_shader_list[layer]['slot'] != slot: + self.selected_shader_list[layer] = self.data.shader_bank_data[layer][slot] + self.selected_shader_list[layer]['slot'] = slot + self.load_shader_layer(layer) else: self.message_handler.set_message('INFO', "shader slot %s:%s is empty"%(layer,slot)) @@ -259,4 +267,203 @@ class Shaders(object): self.osc_client.send_message("/shader/{}/speed".format(str(layer)), amount ) self.selected_speed_list[layer] = amount + # methods for helping dealing with storing and recalling shader parameter frame states + def get_live_frame(self): + #print("get_live_frame: %s" % self.pc.message_handler.shaders.selected_param_list) + import copy #from copy import deepcopy + frame = { + 'selected_shader_slots': [ shader.get('slot',None) for shader in self.selected_shader_list ], + 'shader_params': copy.deepcopy(self.selected_param_list), + 'layer_active_status': copy.deepcopy(self.selected_status_list), + 'feedback_active': self.data.feedback_active, + 'x3_as_speed': self.data.settings['shader']['X3_AS_SPEED']['value'], + 'shader_speeds': copy.deepcopy(self.selected_speed_list), + 'strobe_amount': self.data.settings['shader']['STROBE_AMOUNT']['value'] / 10.0 + } + #print("built frame: %s" % frame['shader_params']) + return frame + + def recall_frame_params(self, preset): + if preset is None: + return + #print("recall_frame_params got: %s" % preset.get('shader_params')) + for (layer, param_list) in enumerate(preset.get('shader_params',[])): + if param_list: + for param,value in enumerate(param_list): + #if (ignored is not None and ignored['shader_params'][layer][param] is not None): + # print ("ignoring %s,%s because value is %s" % (layer,param,ignored['shader_params'][layer][param])) + # continue + if (value is not None): + #print("recalling layer %s param %s: value %s" % (layer,param,value)) + self.data.plugins.actions.call_method_name('set_the_shader_param_%s_layer_%s_continuous' % (param,layer), value) + + if preset.get('feedback_active') is not None: + self.data.feedback_active = preset.get('feedback_active',self.data.feedback_active) + if self.data.feedback_active: + self.data.plugins.actions.call_method_name('enable_feedback') + else: + self.data.plugins.actions.call_method_name('disable_feedback') + + if preset.get('x3_as_speed') is not None: + self.data.settings['shader']['X3_AS_SPEED']['value'] = preset.get('x3_as_speed',self.data.settings['shader']['X3_AS_SPEED']['value']) + """if self.data.settings['shader']['X3_AS_SPEED']['value']: + self.data.plugins.actions.call_method_name('enable_x3_as_speed') + else: + self.data.plugins.actions.call_method_name('disable_x3_as_speed')""" + + for (layer, speed) in enumerate(preset.get('shader_speeds',[])): + if speed is not None: + self.data.plugins.actions.call_method_name('set_shader_speed_layer_%s_amount' % layer, speed) + + if preset.get('strobe_amount') is not None: + self.data.plugins.actions.set_strobe_amount_continuous(preset.get('strobe_amount')) + + def recall_frame(self, preset): + + self.data.settings['shader']['X3_AS_SPEED']['value'] = preset.get('x3_as_speed') + + # x3_as_speed affects preset recall, so do that first + self.recall_frame_params(preset) + + for (layer, slot) in enumerate(preset.get('selected_shader_slots',[])): + if slot is not None: + #print("setting layer %s to slot %s" % (layer, slot)) + self.data.plugins.actions.call_method_name('play_shader_%s_%s' % (layer, slot)) + + for (layer, active) in enumerate(preset.get('layer_active_status',[])): + # print ("got %s layer with status %s " % (layer,active)) + if active=='▶': + self.data.plugins.actions.call_method_name('start_shader_layer_%s' % layer) + else: + self.data.plugins.actions.call_method_name('stop_shader_layer_%s' % layer) + + DEBUG_FRAMES = False + + # overlay frame2 on frame1 + def merge_frames(self, frame1, frame2): + from copy import deepcopy + f = deepcopy(frame1) #frame1.copy() + if self.DEBUG_FRAMES: print("merge_frames: got frame1\t%s" % frame1) + if self.DEBUG_FRAMES: print("merge_frames: got frame2\t%s" % frame2) + for i,f2 in enumerate(frame2['shader_params']): + for i2,p in enumerate(f2): + if p is not None: + f['shader_params'][i][i2] = p + + if frame2['feedback_active'] is not None: + f['feedback_active'] = frame2['feedback_active'] + + if frame2['x3_as_speed'] is not None: + f['x3_as_speed'] = frame2['x3_as_speed'] + + if f.get('shader_speeds') is None: + f['shader_speeds'] = frame2.get('shader_speeds') + else: + for i,s in enumerate(frame2['shader_speeds']): + if s is not None: + f['shader_speeds'][i] = s + + if frame2.get('strobe_amount'): + f['strobe_amount'] = frame2.get('strobe_amount') + + if self.DEBUG_FRAMES: print("merge_frames: got return\t%s" % f) + return f + + def get_frame_ignored(self, frame, ignored): + from copy import deepcopy + f = deepcopy(frame) #frame1.copy() + if self.DEBUG_FRAMES: print("get_frame_ignored: got frame\t%s" % frame) + for i,f2 in enumerate(frame['shader_params']): + for i2,p in enumerate(f2): + if ignored['shader_params'][i][i2] is not None: + f['shader_params'][i][i2] = None + if ignored.get('feedback_active') is not None: + f['feedback_active'] = None + if ignored.get('x3_as_speed') is not None: + f['x3_as_speed'] = None + if ignored.get('shader_speeds') is not None and frame.get('shader_speeds'): + for i,s in enumerate(frame.get('shader_speeds')): + if ignored['shader_speeds'][i] is not None: + f['shader_speeds'][i] = None + if ignored.get('strobe_amount') is not None: + f['strobe_amount'] = None + if self.DEBUG_FRAMES: print("get_frame_ignored: got return\t%s" % f) + return f + + def is_frame_empty(self, frame): + #from copy import deepcopy + #f = deepcopy(frame) #frame1.copy() + if self.DEBUG_FRAMES: print("is_frame_empty: got frame\t%s" % frame) + + if frame.get('feedback_active') is not None: + return False + if frame.get('x3_as_speed') is not None: + return False + if frame.get('strobe_amount') is not None: + return False + + for i,f in enumerate(frame['shader_params']): + for i2,p in enumerate(f): + if p is not None: #ignored['shader_params'][i][i2] is not None: + return False + + if frame.get('shader_speeds') is not None: + for i,f in enumerate(frame['shader_speeds']): + if f is not None: + return False + + if self.DEBUG_FRAMES: print("is_frame_empty: got return true") + return True + + + def get_frame_diff(self, last_frame, current_frame): + if not last_frame: return current_frame + + if self.DEBUG_FRAMES: + print(">>>>get_frame_diff>>>>") + print("last_frame: \t%s" % last_frame['shader_params']) + print("current_frame: \t%s" % current_frame['shader_params']) + + param_values = [[None]*4,[None]*4,[None]*4] + for layer,params in enumerate(current_frame.get('shader_params',[[None]*4]*3)): + #if self.DEBUG_FRAMES: print("got layer %s params: %s" % (layer, params)) + for param,p in enumerate(params): + if p is not None and p != last_frame.get('shader_params')[layer][param]: + if self.DEBUG_FRAMES: print("setting layer %s param %s to %s" % (layer,param,p)) + param_values[layer][param] = p + + if current_frame['feedback_active'] is not None and last_frame['feedback_active'] != current_frame['feedback_active']: + feedback_active = current_frame['feedback_active'] + else: + feedback_active = None + + if current_frame['x3_as_speed'] is not None and last_frame['x3_as_speed'] != current_frame['x3_as_speed']: + x3_as_speed = current_frame['x3_as_speed'] + else: + x3_as_speed = None + + speed_values = [None]*3 + for layer,param in enumerate(current_frame.get('shader_speeds',[None]*3)): + if param is not None and param != last_frame['shader_speeds'][layer]: + speed_values[layer] = param + + if current_frame['strobe_amount'] is not None and last_frame['strobe_amount'] != current_frame['strobe_amount']: + strobe_amount = current_frame['strobe_amount'] + else: + strobe_amount = None + + if self.DEBUG_FRAMES: + print("param_values is\t%s" % param_values) + print("speed_values is\t%s" % speed_values) + + diff = { + 'shader_params': param_values, + 'feedback_active': feedback_active, + 'x3_as_speed': x3_as_speed, + 'shader_speeds': speed_values, + 'strobe_amount': strobe_amount, + } + if self.DEBUG_FRAMES: print("returning\t%s\n^^^^" % diff['shader_params']) + + return diff