diff --git a/zynautoconnect/zynthian_autoconnect.py b/zynautoconnect/zynthian_autoconnect.py index bfd61ffa8..3c6eb6587 100755 --- a/zynautoconnect/zynthian_autoconnect.py +++ b/zynautoconnect/zynthian_autoconnect.py @@ -70,8 +70,6 @@ def unset_alias(self, alias): # Define some Constants and Global Variables # ------------------------------------------------------------------------------- -MAIN_MIX_CHAN = 17 # TODO: Get this from mixer - jclient = None # JACK client thread = None # Thread to check for changed MIDI ports lock = None # Manage concurrence @@ -811,30 +809,23 @@ def audio_autoconnect(): # Chain audio routing for chain_id in chain_manager.chains: routes = chain_manager.get_chain_audio_routing(chain_id) - normalise = 0 in chain_manager.chains[chain_id].audio_out and chain_manager.chains[0].fader_pos == 0 and len( - chain_manager.chains[chain_id].audio_slots) == chain_manager.chains[chain_id].fader_pos - state_manager.zynmixer.normalise( - chain_manager.chains[chain_id].mixer_chan, normalise) for dst in list(routes): if isinstance(dst, int): # Destination is a chain route = routes.pop(dst) dst_chain = chain_manager.get_chain(dst) if dst_chain: - if dst_chain.audio_slots and dst_chain.fader_pos: - for proc in dst_chain.audio_slots[0]: - routes[proc.get_jackname()] = route - elif dst_chain.is_synth(): + if dst_chain.is_synth(): proc = dst_chain.synth_slots[0][0] if proc.type == "Special": routes[proc.get_jackname()] = route else: - if dst == 0: - for name in list(route): - if name.startswith('zynmixer:output'): - # Use mixer internal normalisation - route.remove(name) - routes[f"zynmixer:input_{dst_chain.mixer_chan + 1:02d}"] = route + for proc in chain_manager.chains[dst].audio_slots[0]: + #TODO: Handle internal normalisation + jackname = proc.get_jackname() + if jackname.startswith("zynmixer"): + jackname += f":input_{proc.mixer_chan:02d}" + routes[jackname] = route for dst in routes: if dst in sidechain_ports: # This is an exact match so we do want to route exactly this @@ -861,16 +852,20 @@ def audio_autoconnect(): dst = dst_ports[min(i, dst_count - 1)] required_routes[dst.name].add(src.name) - # Connect metronome to aux - required_routes[f"zynmixer:input_{MAIN_MIX_CHAN}a"].add("zynseq:metronome") - required_routes[f"zynmixer:input_{MAIN_MIX_CHAN}b"].add("zynseq:metronome") - # Connect global audio player to aux - if state_manager.audio_player and state_manager.audio_player.jackname: - ports = jclient.get_ports( - state_manager.audio_player.jackname, is_output=True, is_audio=True) - required_routes[f"zynmixer:input_{MAIN_MIX_CHAN}a"].add(ports[0].name) - required_routes[f"zynmixer:input_{MAIN_MIX_CHAN}b"].add(ports[1].name) + try: + # Connect metronome to aux + required_routes[f"zynmixer_buses:input_00a"].add("zynseq:metronome") + required_routes[f"zynmixer_buses:input_00b"].add("zynseq:metronome") + + # Connect global audio player to aux + if state_manager.audio_player and state_manager.audio_player.jackname: + ports = jclient.get_ports( + state_manager.audio_player.jackname, is_output=True, is_audio=True) + required_routes[f"zynmixer_buses:input_00a"].add(ports[0].name) + required_routes[f"zynmixer_buses:input_00b"].add(ports[1].name) + except Exception as e: + logging.warning(e) # Connect inputs to aubionotes if zynthian_gui_config.midi_aubionotes_enabled: @@ -936,9 +931,9 @@ def audio_connect_ffmpeg(timeout=2.0): try: # TODO: Do we want post fader, post effects feed? jclient.connect( - f"zynmixer:output_{MAIN_MIX_CHAN}a", "ffmpeg:input_1") + f"zynmixer_buses:input_00a", "ffmpeg:input_1") jclient.connect( - f"zynmixer:output_{MAIN_MIX_CHAN}b", "ffmpeg:input_2") + f"zynmixer_buses:input_00b", "ffmpeg:input_2") return except: sleep(0.1) diff --git a/zyngine/__init__.py b/zyngine/__init__.py index 1cd1ae2db..3be1a592b 100644 --- a/zyngine/__init__.py +++ b/zyngine/__init__.py @@ -3,6 +3,7 @@ "zynthian_controller", "zynthian_lv2", "zynthian_engine", + "zynthian_engine_audio_mixer", "zynthian_engine_zynaddsubfx", "zynthian_engine_linuxsampler", "zynthian_engine_fluidsynth", @@ -25,6 +26,7 @@ from zyngine.zynthian_controller import * from zyngine.zynthian_lv2 import * from zyngine.zynthian_engine import * +from zyngine.zynthian_engine_audio_mixer import * from zyngine.zynthian_engine_zynaddsubfx import * from zyngine.zynthian_engine_linuxsampler import * from zyngine.zynthian_engine_fluidsynth import * diff --git a/zyngine/zynthian_audio_recorder.py b/zyngine/zynthian_audio_recorder.py index 7548a0315..3c431d2ea 100644 --- a/zyngine/zynthian_audio_recorder.py +++ b/zyngine/zynthian_audio_recorder.py @@ -108,14 +108,14 @@ def start_recording(self, processor=None): if self.armed: for port in sorted(self.armed): cmd.append("--port") - cmd.append(f"zynmixer:output_{port + 1:02d}a") + cmd.append(f"zynmixer_chans:output_{port + 1:02d}a") cmd.append("--port") - cmd.append(f"zynmixer:output_{port + 1:02d}b") + cmd.append(f"zynmixer_chans:output_{port + 1:02d}b") else: cmd.append("--port") - cmd.append("zynmixer:output_17a") + cmd.append("zynmixer_buses:output_0a") cmd.append("--port") - cmd.append("zynmixer:output_17b") + cmd.append("zynmixer_buses:output_0b") self.filename = self.get_new_filename() cmd.append(self.filename) diff --git a/zyngine/zynthian_chain.py b/zyngine/zynthian_chain.py index e319653cb..f123b0a56 100644 --- a/zyngine/zynthian_chain.py +++ b/zyngine/zynthian_chain.py @@ -28,19 +28,16 @@ # Zynthian specific modules import zynautoconnect +from zyngine.zynthian_processor import zynthian_processor from zyncoder.zyncore import lib_zyncore -CHAIN_MODE_SERIES = 0 -CHAIN_MODE_PARALLEL = 1 - - class zynthian_chain: # ------------------------------------------------------------------------ # Initialization # ------------------------------------------------------------------------ - def __init__(self, chain_id, midi_chan=None, midi_thru=False, audio_thru=False): + def __init__(self, chain_id, midi_chan=None, midi_thru=False, audio_thru=False, fx_loop=False): """ Create an instance of a chain A chain contains zero or more slots. @@ -54,6 +51,7 @@ def __init__(self, chain_id, midi_chan=None, midi_thru=False, audio_thru=False): midi_chan : Optional MIDI channel for chain input / control midi_thru : True to enable MIDI thru for empty chain audio_thru : True to enable audio thru for empty chain + fx_loop: True to configure as an effects loop chain """ # Each slot contains a list of parallel processors @@ -61,19 +59,19 @@ def __init__(self, chain_id, midi_chan=None, midi_thru=False, audio_thru=False): # Synth/generator/special slots (should be single slot) self.synth_slots = [] self.audio_slots = [] # Audio subchain (list of lists of processors) - self.fader_pos = 0 # Position of fader in audio effects chain self.chain_id = chain_id # Chain's ID # Chain's MIDI channel - None for purely audio chain, 0xffff for *All Chains* self.midi_chan = midi_chan - self.mixer_chan = None self.zmop_index = None self.midi_thru = midi_thru # True to pass MIDI if chain empty self.audio_thru = audio_thru # True to pass audio if chain empty + self.fx_loop = fx_loop # True if chain is fed from fxsend and feeds fxreturn self.midi_in = [] self.midi_out = [] self.audio_in = [] self.audio_out = [] + self.record_audio = False self.status = "" # Arbitary status text self.current_processor = None # Selected processor object @@ -99,7 +97,10 @@ def reset(self): self.audio_thru = True else: self.title = "" - self.audio_in = [1, 2] + if self.fx_loop: + self.audio_in = [] #TODO: Should this be fx send? + elif self.audio_thru: + self.audio_in = [1, 2] self.audio_out = [0] if self.is_midi(): @@ -138,15 +139,6 @@ def get_type(self): # Chain Management # ---------------------------------------------------------------------------- - def set_mixer_chan(self, chan): - """Set chain mixer channel - - chan : Mixer channel 0..Max Channels or None - """ - - self.mixer_chan = chan - self.rebuild_audio_graph() - def set_zmop_options(self): if self.zmop_index is not None and len(self.synth_slots) > 0: # IMPORTANT!!! Synth chains drop CC & PC messages @@ -301,28 +293,34 @@ def rebuild_audio_graph(self): for i, slot in enumerate(self.audio_slots): for processor in slot: sources = [] - if i < self.fader_pos: - if i == 0: - # First slot fed from synth or chain input - if self.synth_slots: - for proc in self.synth_slots[-1]: - sources.append(proc.get_jackname()) - elif self.audio_thru: - sources = self.get_input_pairs() - self.audio_routes[processor.get_jackname()] = sources - else: - for prev_proc in self.audio_slots[i - 1]: - sources.append(prev_proc.get_jackname()) - self.audio_routes[processor.get_jackname()] = sources + if processor.bypass: + continue + if i == 0: + # First slot fed from synth or chain input + if self.synth_slots: + for proc in self.synth_slots[-1]: + sources.append(proc.get_jackname()) + elif self.fx_loop: + for am_slot in self.audio_slots: + if am_slot[0].eng_code == "AM": + sources = [f"zynmixer_chans:send_{am_slot[0].mixer_chan:02d}"] + break + elif self.audio_thru: + sources = self.get_input_pairs() + jackname = processor.get_jackname() + if jackname.startswith("zynmixer"): + jackname += f":input_{processor.mixer_chan:02d}" + self.audio_routes[jackname] = sources else: - # Post fader - if i == self.fader_pos: - self.audio_routes[processor.get_jackname()] = [ - f"zynmixer:output_{self.mixer_chan + 1:02d}"] - else: - for prev_proc in self.audio_slots[i - 1]: - sources.append(prev_proc.get_jackname()) - self.audio_routes[processor.get_jackname()] = sources + for prev_proc in self.audio_slots[i - 1]: + jackname = prev_proc.get_jackname() + if jackname.startswith("zynmixer"): + jackname += f":output_{prev_proc.mixer_chan:02d}" + sources.append(jackname) + jackname = processor.get_jackname() + if jackname.startswith("zynmixer"): + jackname += f":input_{processor.mixer_chan:02d}" + self.audio_routes[jackname] = sources # Add special processor inputs if self.is_synth(): @@ -331,35 +329,17 @@ def rebuild_audio_graph(self): sources = self.get_input_pairs() self.audio_routes[processor.get_jackname()] = sources - if self.mixer_chan is not None: - mixer_source = [] - if self.fader_pos: - # Routing from last audio processor - for source in self.audio_slots[self.fader_pos - 1]: - mixer_source.append(source.get_jackname()) - elif self.synth_slots: - # Routing from synth processor - for proc in self.synth_slots[0]: - mixer_source.append(proc.get_jackname()) - elif self.audio_thru: - # Routing from capture ports or main chain - mixer_source = self.get_input_pairs() - # Connect end of pre-fader chain - self.audio_routes[f"zynmixer:input_{self.mixer_chan + 1:02d}"] = mixer_source - - # Connect end of post-fader chain - if self.fader_pos < len(self.audio_slots): - # Use end of post fader chain - slot = self.audio_slots[-1] - sources = [] - for processor in slot: - sources.append(processor.get_jackname()) - else: - # Use mixer channel output - # if self.mixer_chan < 16: #TODO: Get main mixbus channel from zynmixer - # sources = [] # Do not route - zynmixer will normalise outputs to main mix bus - # else: - sources = [f"zynmixer:output_{self.mixer_chan + 1:02d}"] + # Connect end of chain + # Use end of post fader chain + + if self.audio_slots: + slot = self.audio_slots[-1] + sources = [] + for processor in slot: + jackname = processor.get_jackname() + if jackname.startswith("zynmixer"): + jackname += f":output_{processor.mixer_chan:02d}" + sources.append(jackname) for output in self.get_audio_out(): self.audio_routes[output] = sources.copy() @@ -433,16 +413,6 @@ def get_audio_out(self): """Get list of audio playback port names""" return self.audio_out.copy() - audio_out = [] - for output in self.audio_out: - if output == 0: - if self.mixer_chan < 17: - audio_out.append("zynmixer:input_18") - else: - audio_out.append("system:playback_[1,2]$") - else: - audio_out.append(output) - return audio_out def toggle_audio_out(self, out): """Toggle chain audio output @@ -495,7 +465,7 @@ def toggle_midi_out(self, dest): def is_audio(self): """Returns True if chain is processes audio""" - return self.mixer_chan is not None + return self.synth_slots or self.audio_thru def is_midi(self): """Returns True if chain processes MIDI""" @@ -524,10 +494,6 @@ def get_slot_count(self, type=None): return len(self.midi_slots) elif type == "Audio Effect": return len(self.audio_slots) - elif type == "Pre Fader": - return self.fader_pos - elif type == "Post Fader": - return len(self.audio_slots) - self.fader_pos elif type == "MIDI Synth": return len(self.synth_slots) else: @@ -589,11 +555,10 @@ def get_processors(self, type=None, slot=None): processors = slots[slot] return processors - def insert_processor(self, processor, parallel=False, slot=None): + def insert_processor(self, processor, slot=None): """Insert a processor in the chain processor : processor object to insert - parallel : True to add in parallel (same slot) else create new slot (Default: series) slot : Position (slot) to insert within subchain (Default: End of chain) Returns : True if processor added to chain """ @@ -603,11 +568,15 @@ def insert_processor(self, processor, parallel=False, slot=None): slots.append([processor]) else: if slot is None or slot < 0 or slot > len(slots): - slot = len(slots) - 1 - if parallel: - slots[slot].append(processor) + # Append to end of chain + if processor.type == "Audio Effect": + for idx in range(len(slots)): + if slots[idx][0].eng_code == "AM": + break + slots.insert(idx, [processor]) else: - slots.insert(slot + 1, [processor]) + # Add parallel processor + slots[slot].append(processor) processor.set_chain(self) processor.set_midi_chan(self.midi_chan) @@ -657,8 +626,6 @@ def remove_processor(self, processor): slots[slot].remove(processor) if len(slots[slot]) == 0: slots.pop(slot) - if processor.type == "Audio Effect" and slot < self.fader_pos: - self.fader_pos -= 1 processor.set_chain(None) if processor.engine: @@ -684,8 +651,6 @@ def remove_processor(self, processor): def remove_all_processors(self): """Remove all processors from chain - - stop_engines : True to stop the processors' worker engines """ for processor in self.get_processors(): @@ -696,37 +661,42 @@ def nudge_processor(self, processor, up): slots = self.get_slots_by_type(processor.type) cur_slot = self.get_slot(processor) parallel = len(slots[cur_slot]) > 1 - is_audio = processor.type == "Audio Effect" + is_mixer_strip = processor.eng_code == "AM" + if up: - if parallel: + if is_mixer_strip: + slots.pop(cur_slot) + slots.insert(cur_slot - 1, [processor]) + elif parallel: slots[cur_slot].remove(processor) - slots.insert(cur_slot, [processor]) - if is_audio and cur_slot < self.fader_pos: - self.fader_pos += 1 - elif is_audio and cur_slot == self.fader_pos: - self.fader_pos += 1 + if slots[cur_slot][0].eng_code == "AM": + slots.insert(cur_slot - 1, [processor]) + else: + slots.insert(cur_slot, [processor]) elif cur_slot > 0: slots.pop(cur_slot) - slots[cur_slot - 1].append(processor) - if is_audio and cur_slot < self.fader_pos: - self.fader_pos -= 1 + if slots[cur_slot - 1][0].eng_code == "AM": + slots.insert(cur_slot - 1, [processor]) + else: + slots[cur_slot - 1].append(processor) else: return False else: - if parallel: - slots[cur_slot].remove(processor) - slots.insert(cur_slot + 1, [processor]) - if is_audio and cur_slot < self.fader_pos: - self.fader_pos += 1 - elif is_audio and cur_slot + 1 == self.fader_pos: - self.fader_pos -= 1 - elif cur_slot + 1 < len(slots): + if is_mixer_strip: slots.pop(cur_slot) - slots[cur_slot].append(processor) - if is_audio and cur_slot < self.fader_pos: - self.fader_pos -= 1 + slots.insert(cur_slot + 1, [processor]) + elif parallel: + slots[cur_slot].remove(processor) + if slots[cur_slot + 1][0].eng_code == "AM": + slots.insert(cur_slot + 1, [processor]) + else: + slots.insert(cur_slot, [processor]) else: - return False + slots.pop(cur_slot) + if slots[cur_slot][0].eng_code == "AM": + slots.insert(cur_slot + 1, [processor]) + else: + slots[cur_slot].append(processor) self.rebuild_graph() except: @@ -810,11 +780,9 @@ def get_state(self): "midi_chan": self.midi_chan, "midi_thru": self.midi_thru, "audio_thru": self.audio_thru, - "mixer_chan": self.mixer_chan, "zmop_index": self.zmop_index, "cc_route": cc_route, - "slots": slots_states, - "fader_pos": self.fader_pos + "slots": slots_states } return state diff --git a/zyngine/zynthian_chain_manager.py b/zyngine/zynthian_chain_manager.py index 5cc8563c5..200dca206 100644 --- a/zyngine/zynthian_chain_manager.py +++ b/zyngine/zynthian_chain_manager.py @@ -66,14 +66,15 @@ "BF": zynthian_engine_setbfree, 'JV': zynthian_engine_jalv, "AE": zynthian_engine_aeolus, - 'PT': zynthian_engine_pianoteq, + "PT": zynthian_engine_pianoteq, "AP": zynthian_engine_audioplayer, "SL": zynthian_engine_sooperlooper, - 'SX': zynthian_engine_sysex, - 'MC': zynthian_engine_midi_control, - 'PD': zynthian_engine_puredata, - 'MD': zynthian_engine_modui, - 'IR': zynthian_engine_inet_radio + "SX": zynthian_engine_sysex, + "MC": zynthian_engine_midi_control, + "PD": zynthian_engine_puredata, + "MD": zynthian_engine_modui, + "IR": zynthian_engine_inet_radio, + "AM": zynthian_engine_audio_mixer } # ---------------------------------------------------------------------------- @@ -174,18 +175,17 @@ def save_engine_info(cls): # Chain Management # ------------------------------------------------------------------------ - def add_chain(self, chain_id, midi_chan=None, midi_thru=False, audio_thru=False, mixer_chan=None, zmop_index=None, title="", chain_pos=None, fast_refresh=True): + def add_chain(self, chain_id, midi_chan=None, midi_thru=False, audio_thru=False, fx_loop=False, zmop_index=None, title="", chain_pos=None): """Add a chain chain_id: UID of chain (None to get next available) midi_chan : MIDI channel associated with chain midi_thru : True to enable MIDI thru for empty chain (Default: False) audio_thru : True to enable audio thru for empty chain (Default: False) - mixer_chan : Mixer channel (Default: None) + fx_loop : True to add an audio effects send/return loop zmop_index : MIDI router output (Default: None) title : Chain title (Default: None) chain_pos : Position to insert chain (Default: End) - fast_refresh : False to trigger slow autoconnect (Default: Fast autoconnect) Returns : Chain ID or None if chain could not be created """ @@ -202,34 +202,23 @@ def add_chain(self, chain_id, midi_chan=None, midi_thru=False, audio_thru=False, if chain_id == 0: # main midi_thru = False audio_thru = True - mixer_chan = self.state_manager.zynmixer.MAX_NUM_CHANNELS - 1 # If the chain already exists, update and return if chain_id in self.chains: self.chains[chain_id].midi_thru = midi_thru self.chains[chain_id].audio_thru = audio_thru + self.chains[chain_id].fx_loop = fx_loop self.state_manager.end_busy("add_chain") return chain_id # Create chain instance - chain = zynthian_chain(chain_id, midi_chan, midi_thru, audio_thru) + chain = zynthian_chain(chain_id, midi_chan, midi_thru, audio_thru, fx_loop) if not chain: return None self.chains[chain_id] = chain # Setup chain chain.set_title(title) - # If a mixer_chan is specified (restore from state), setup mixer_chan - if mixer_chan is not None: - chain.set_mixer_chan(mixer_chan) - # else, if audio_thru enabled, setup a mixer_chan - elif audio_thru: - try: - chain.set_mixer_chan(self.get_next_free_mixer_chan()) - except Exception as e: - logging.warning(e) - self.state_manager.end_busy("add_chain") - return None # Setup MIDI routing if isinstance(midi_chan, int): @@ -263,15 +252,16 @@ def add_chain(self, chain_id, midi_chan=None, midi_thru=False, audio_thru=False, # Add to chain index (sorted!) if chain_pos is None: - chain_pos = self.get_chain_index(0) + chain_pos = 0 + for chain_pos, id in enumerate(self.ordered_chain_ids): + if id == 0: + break + if not fx_loop and self.chains[id].fx_loop: + break self.ordered_chain_ids.insert(chain_pos, chain_id) - chain.rebuild_graph() - zynautoconnect.request_audio_connect(fast_refresh) - zynautoconnect.request_midi_connect(fast_refresh) - logging.debug( - f"ADDED CHAIN {chain_id} => midi_chan={chain.midi_chan}, mixer_chan={chain.mixer_chan}, zmop_index={chain.zmop_index}") + f"ADDED CHAIN {chain_id} => midi_chan={chain.midi_chan}, zmop_index={chain.zmop_index}") # logging.debug(f"ordered_chain_ids = {self.ordered_chain_ids}") # logging.debug(f"midi_chan_2_chain_ids = {self.midi_chan_2_chain_ids}") @@ -296,17 +286,17 @@ def add_chain_from_state(self, chain_id, chain_state): audio_thru = chain_state['audio_thru'] else: audio_thru = False - if 'mixer_chan' in chain_state: - mixer_chan = chain_state['mixer_chan'] + if 'fx_loop' in chain_state: + fx_loop = chain_state['fx_loop'] else: - mixer_chan = None + fx_loop = False if 'zmop_index' in chain_state: zmop_index = chain_state['zmop_index'] else: zmop_index = None - chain_id = self.add_chain(chain_id, midi_chan=midi_chan, midi_thru=midi_thru, audio_thru=audio_thru, - mixer_chan=mixer_chan, zmop_index=zmop_index, title=title, fast_refresh=False) + chain_id = self.add_chain(chain_id, midi_chan=midi_chan, midi_thru=midi_thru, audio_thru=audio_thru, fx_loop=fx_loop, + zmop_index=zmop_index, title=title) # Set CC route state zmop_index = self.chains[chain_id].zmop_index @@ -353,28 +343,22 @@ def remove_chain(self, chain_id, stop_engines=True, fast_refresh=True): self.midi_chan_2_chain_ids[mc].remove(chain_id) lib_zyncore.ui_send_ccontrol_change(mc, 120, 0) - if chain.mixer_chan is not None: - mute = self.state_manager.zynmixer.get_mute(chain.mixer_chan) - self.state_manager.zynmixer.set_mute( - chain.mixer_chan, True, True) + if chain.is_audio(): + #TODO: Mute chain whilst removing + pass for processor in chain.get_processors(): - self.remove_processor(chain_id, processor, False, False) + self.remove_processor(chain_id, processor, False) chain.reset() if chain_id != 0: - if chain.mixer_chan is not None: - self.state_manager.zynmixer.reset(chain.mixer_chan) - self.state_manager.audio_recorder.unarm(chain.mixer_chan) + if chain.is_audio(): + #TODO: Disable audio_recorder.unarm + pass self.chains.pop(chain_id) - self.state_manager.zynmixer.set_mute( - chain.mixer_chan, False, True) del chain if chain_id in self.ordered_chain_ids: self.ordered_chain_ids.remove(chain_id) - elif chain.mixer_chan is not None: - self.state_manager.zynmixer.set_mute( - chain.mixer_chan, mute, True) zynautoconnect.request_audio_connect(fast_refresh) zynautoconnect.request_midi_connect(fast_refresh) @@ -478,8 +462,9 @@ def get_chain_id_by_mixer_chan(self, chan): """Get a chain by the mixer channel""" for chain_id, chain in self.chains.items(): - if chain.mixer_chan is not None and chain.mixer_chan == chan: - return chain_id + for slot in chain.audio_slots: + if slot[0].eng_code == "AM": + return slot[0].mixer_chan return None # ------------------------------------------------------------------------ @@ -787,15 +772,13 @@ def get_available_processor_id(self): else: return 1 - def add_processor(self, chain_id, eng_code, parallel=False, slot=None, proc_id=None, post_fader=False, fast_refresh=True, eng_config=None): + def add_processor(self, chain_id, eng_code, slot=None, proc_id=None, eng_config=None): """Add a processor to a chain chain : Chain ID eng_code : Engine's code - parallel : True to add in parallel (same slot) else create new slot (Default: series) slot : Slot (position) within subchain (0..last slot, Default: last slot) proc_id : Processor UID (Default: Use next available ID) - post_fader : True to move the fader position fast_refresh : False to trigger slow autoconnect (Default: Fast autoconnect) eng_config: Extended configuration for the engine (optional) Returns : processor object or None on failure @@ -828,25 +811,16 @@ def add_processor(self, chain_id, eng_code, parallel=False, slot=None, proc_id=N chain = self.chains[chain_id] # Add proc early to allow engines to add more as required, e.g. Aeolus self.processors[proc_id] = processor - if chain.insert_processor(processor, parallel, slot): - if not parallel and not post_fader and processor.type == "Audio Effect": - chain.fader_pos += 1 + if chain.insert_processor(processor, slot): # TODO: Fails to detect MIDI only chains in snapshots - if chain.mixer_chan is None and processor.type != "MIDI Tool": - try: - chain.mixer_chan = self.get_next_free_mixer_chan() - except Exception as e: - logging.warning(e) - return None engine = self.start_engine(processor, eng_code, eng_config) if engine: - chain.rebuild_graph() + #chain.rebuild_graph() # Update group chains for src_chain in self.chains.values(): if chain_id in src_chain.audio_out: src_chain.rebuild_graph() - zynautoconnect.request_audio_connect(fast_refresh) - zynautoconnect.request_midi_connect(fast_refresh) + chain.rebuild_graph() # Success!! => Return processor self.state_manager.end_busy("add_processor") return processor @@ -871,7 +845,7 @@ def nudge_processor(self, chain_id, processor, up): if chain_id in src_chain.audio_out: src_chain.rebuild_graph() - if chain.mixer_chan is not None: + if chain.is_audio(): # Audio chain so mute main output whilst making change (blunt but effective) mute = self.state_manager.zynmixer.get_mute(255) self.state_manager.zynmixer.set_mute(255, True, False) @@ -880,13 +854,12 @@ def nudge_processor(self, chain_id, processor, up): zynautoconnect.request_midi_connect(True) return True - def remove_processor(self, chain_id, processor, stop_engine=True, autoroute=True): + def remove_processor(self, chain_id, processor, stop_engine=True): """Remove a processor from a chain chain : Chain id processor : Instance of processor stop_engine : True to stop unused engine - autoroute : True to trigger immediate autoconnect (Default: No autoconnect) Returns : True on success """ @@ -922,12 +895,9 @@ def remove_processor(self, chain_id, processor, stop_engine=True, autoroute=True if stop_engine: self.stop_unused_engines() - if autoroute: - # Update chain routing (may have effected lots of chains) - for chain in self.chains.values(): - chain.rebuild_graph() - zynautoconnect.request_audio_connect() - zynautoconnect.request_midi_connect() + # Update chain routing (may have effected lots of chains) + for chain in self.chains.values(): + chain.rebuild_graph() self.state_manager.end_busy("remove_processor") return success @@ -1188,17 +1158,9 @@ def set_state(self, state, engine_config, merge=False): eng_config = engine_config[eng_code] except: eng_config = None - # Use index to identify first proc in slot (add in series) - others are added in parallel - if index: - mode = CHAIN_MODE_PARALLEL - else: - mode = CHAIN_MODE_SERIES - self.add_processor(chain_id, eng_code, mode, proc_id=int( + # Use index to identify first proc in slot (add in series) + self.add_processor(chain_id, eng_code, proc_id=int( proc_id), fast_refresh=False, eng_config=eng_config) - if "fader_pos" in chain_state and self.get_slot_count(chain_id, "Audio Effect") >= chain_state["fader_pos"]: - self.chains[chain_id].fader_pos = chain_state["fader_pos"] - else: - self.chains[chain_id].fader_pos = 0 self.state_manager.end_busy("set_chain_state") @@ -1534,32 +1496,6 @@ def get_num_chains_midi_chan(self, chan): except: return 0 - def get_free_mixer_chans(self): - """Get list of unused mixer channels""" - - free_chans = list(range(MAX_NUM_MIXER_CHANS)) - for chain in self.chains: - try: - free_chans.remove(self.chains[chain].mixer_chan) - except: - pass - return free_chans - - def get_next_free_mixer_chan(self, chan=0): - """Get next unused mixer channel - - chan : mixer channel to search from (Default: 0) - """ - - free_chans = self.get_free_mixer_chans() - for i in range(chan, MAX_NUM_MIXER_CHANS): - if i in free_chans: - return i - for i in range(chan): - if i in free_chans: - return i - raise Exception("No available free mixer channels!") - def is_free_zmop_index(self, zmop_index): """Get next unused zmop index """ diff --git a/zyngine/zynthian_engine.py b/zyngine/zynthian_engine.py index e0d51a2c9..3d5207532 100644 --- a/zyngine/zynthian_engine.py +++ b/zyngine/zynthian_engine.py @@ -567,6 +567,7 @@ def load_preset_favs(self): else: logging.warning( "Can't load preset favorites until the engine have a nickname!") + self.preset_favs = {} # --------------------------------------------------------------------------- # Controllers Management diff --git a/zyngine/zynthian_engine_aeolus.py b/zyngine/zynthian_engine_aeolus.py index a2ea33ca9..8b5003e32 100644 --- a/zyngine/zynthian_engine_aeolus.py +++ b/zyngine/zynthian_engine_aeolus.py @@ -344,7 +344,6 @@ def start(self): chain = chain_manager.get_chain(chain_id) if proc_i: chain.audio_out = [] - chain.mixer_chan = None processor.refresh_controllers() diff --git a/zyngine/zynthian_engine_audio_mixer.py b/zyngine/zynthian_engine_audio_mixer.py index 5679b120d..bee47d048 100644 --- a/zyngine/zynthian_engine_audio_mixer.py +++ b/zyngine/zynthian_engine_audio_mixer.py @@ -31,21 +31,34 @@ from zyngine.zynthian_signal_manager import zynsigman # ------------------------------------------------------------------------------- -# Zynmixer Library Wrapper +# Zynmixer Library Wrapper and processor # ------------------------------------------------------------------------------- - -class zynmixer(zynthian_engine): +class zynthian_engine_audio_mixer(zynthian_engine): # Subsignals are defined inside each module. Here we define audio_mixer subsignals: SS_ZCTRL_SET_VALUE = 1 + # Controller Screens + _ctrl_screens = [ + ['main', ['level', 'balance', 'mute', 'solo']], + ['aux', ['mono', 'phase', 'ms', 'record']] + ] + # Function to initialize library - def __init__(self): - super().__init__() + def __init__(self, state_manager): + super().__init__(state_manager) + self.type = "Audio Effect" + self.name = "AudioMixer" self.lib_zynmixer = ctypes.cdll.LoadLibrary( "/zynthian/zynthian-ui/zynlibs/zynmixer/build/libzynmixer.so") - self.lib_zynmixer.init() + self.midi_learn_zctrl = None + self.fx_loop = False # Used as temporary flag when adding processor (bodge) + + self.lib_zynmixer.addStrip.argtypes = [ctypes.c_uint8] + self.lib_zynmixer.addStrip.restype = ctypes.c_int8 + self.lib_zynmixer.removeStrip.argtypes = [ctypes.c_uint8] + self.lib_zynmixer.removeStrip.restype = ctypes.c_int8 self.lib_zynmixer.setLevel.argtypes = [ctypes.c_uint8, ctypes.c_float] self.lib_zynmixer.getLevel.argtypes = [ctypes.c_uint8] @@ -77,6 +90,10 @@ def __init__(self): self.lib_zynmixer.getPhase.argtypes = [ctypes.c_uint8] self.lib_zynmixer.getPhase.restype = ctypes.c_uint8 + self.lib_zynmixer.setSend.argtypes = [ctypes.c_uint8, ctypes.c_uint8, ctypes.c_float] + self.lib_zynmixer.getSend.argtypes = [ctypes.c_uint8, ctypes.c_uint8] + self.lib_zynmixer.getSend.restype = ctypes.c_float + self.lib_zynmixer.setNormalise.argtypes = [ ctypes.c_uint8, ctypes.c_uint8] self.lib_zynmixer.getNormalise.argtypes = [ctypes.c_uint8] @@ -84,12 +101,6 @@ def __init__(self): self.lib_zynmixer.reset.argtypes = [ctypes.c_uint8] - self.lib_zynmixer.isChannelRouted.argtypes = [ctypes.c_uint8] - self.lib_zynmixer.isChannelRouted.restype = ctypes.c_uint8 - - self.lib_zynmixer.isChannelOutRouted.argtypes = [ctypes.c_uint8] - self.lib_zynmixer.isChannelOutRouted.restype = ctypes.c_uint8 - self.lib_zynmixer.getDpm.argtypes = [ctypes.c_uint8, ctypes.c_uint8] self.lib_zynmixer.getDpm.restype = ctypes.c_float @@ -110,85 +121,161 @@ def __init__(self): # List of learned {cc:zctrl} indexed by learned MIDI channel self.learned_cc = [dict() for x in range(16)] - # List of {symbol:zctrl,...} indexed by mixer strip index - self.zctrls = [] - for i in range(self.MAX_NUM_CHANNELS): - strip_dict = { + def stop(self): + #TODO: Implement stop + pass + + def get_controllers_dict(self, processor): + if not processor.controllers_dict: + processor.controllers_dict = { 'level': zynthian_controller(self, 'level', { 'is_integer': False, 'value_max': 1.0, 'value_default': 0.8, - 'value': self.get_level(i), - 'graph_path': [i, 'level'] + 'value': self.get_level(processor.mixer_chan), + 'processor': processor, + 'graph_path': [processor.mixer_chan, 'level'] }), 'balance': zynthian_controller(self, 'balance', { 'is_integer': False, 'value_min': -1.0, 'value_max': 1.0, 'value_default': 0.0, - 'value': self.get_balance(i), - 'graph_path': [i, 'balance'] + 'value': self.get_balance(processor.mixer_chan), + 'processor': processor, + 'graph_path': [processor.mixer_chan, 'balance'] }), 'mute': zynthian_controller(self, 'mute', { 'is_toggle': True, 'value_max': 1, 'value_default': 0, - 'value': self.get_mute(i), - 'graph_path': [i, 'mute'] + 'value': self.get_mute(processor.mixer_chan), + 'graph_path': [processor.mixer_chan, 'mute'], + 'processor': processor, + 'labels': ['off', 'on'] }), 'solo': zynthian_controller(self, 'solo', { 'is_toggle': True, 'value_max': 1, 'value_default': 0, - 'value': self.get_solo(i), - 'graph_path': [i, 'solo'] + 'value': self.get_solo(processor.mixer_chan), + 'graph_path': [processor.mixer_chan, 'solo'], + 'processor': processor, + 'labels': ['off', 'on'] }), 'mono': zynthian_controller(self, 'mono', { 'is_toggle': True, 'value_max': 1, 'value_default': 0, - 'value': self.get_mono(i), - 'graph_path': [i, 'mono'] + 'value': self.get_mono(processor.mixer_chan), + 'graph_path': [processor.mixer_chan, 'mono'], + 'processor': processor, + 'labels': ['off', 'on'] }), - 'ms': zynthian_controller(self, 'm+s', { + 'ms': zynthian_controller(self, 'ms', { 'is_toggle': True, 'value_max': 1, 'value_default': 0, - 'value': self.get_ms(i), - 'graph_path': [i, 'ms'] + 'value': self.get_ms(processor.mixer_chan), + 'graph_path': [processor.mixer_chan, 'ms'], + 'labels': ['off', 'on'], + 'processor': processor, + 'name': "M+S" }), 'phase': zynthian_controller(self, 'phase', { 'is_toggle': True, 'value_max': 1, 'value_default': 0, - 'value': self.get_phase(i), - 'graph_path': [i, 'phase'] + 'value': self.get_phase(processor.mixer_chan), + 'graph_path': [processor.mixer_chan, 'phase'], + 'processor': processor, + 'labels': ['off', 'on'] + }), + 'record': zynthian_controller(self, 'record', { + 'is_toggle': True, + 'value_max': 1, + 'value_default': 0, + 'value': 0, + 'graph_path': [processor.mixer_chan, 'record'], + 'processor': processor, + 'labels': ['off', 'on'] }) } - self.zctrls.append(strip_dict) - - self.midi_learn_zctrl = None - self.midi_learn_cb = None - - def get_controllers_dict(self, processor): - try: - return self.zctrls[processor.mixer_chan] - except: - return None - - def get_learned_cc(self, zctrl): - for chan in range(16): - for cc in self.learned_cc[chan]: - if zctrl == self.learned_cc[chan][cc]: - return [chan, cc] + return processor.controllers_dict + + def update_fx_send(self, processor, send): + symbol = f"send_{send:02d}" + processor.controllers_dict[symbol] = zynthian_controller(self, symbol, { + 'name': f'send {send} level', + 'value_max': 1.0, + 'value_default': 0.0, + 'value': self.get_send(processor.mixer_chan, send), + 'graph_path': [processor.mixer_chan, "send", send] + }) + processor.ctrl_screens_dict[f"send {send}"] = [processor.controllers_dict[symbol]] + + def add_processor(self, processor): + self.processors.append(processor) + processor.fx_loop = processor.engine.fx_loop + if processor.chain_id: + processor.mixer_chan = self.add_strip(processor.fx_loop) + else: + processor.mixer_chan = 0 + if processor.fx_loop or processor.chain_id == 0: + processor.jackname = "zynmixer_buses" + else: + processor.jackname = "zynmixer_chans" + processor.refresh_controllers() + if not processor.fx_loop: + for proc in self.processors: + if proc.chain_id and proc.fx_loop: + self.update_fx_send(processor, proc.mixer_chan) + return + + def remove_processor(self, processor): + super().remove_processor(processor) + self.lib_zynmixer.removeStrip(processor.mixer_chan) + + def set_extended_config(self, config): + if config is None: + return + if "fx_loop" in config: + self.fx_loop = config["fx_loop"] def send_controller_value(self, zctrl): try: - getattr(self, f'set_{zctrl.symbol}')( - zctrl.graph_path[0], zctrl.value, False) + if zctrl.graph_path[1] == "send": + self.set_send(zctrl.graph_path[0], zctrl.graph_path[2], zctrl.value, False) + elif zctrl.symbol == "record": + if zctrl.value: + self.state_manager.audio_recorder.arm(zctrl.graph_path[0]) + else: + self.state_manager.audio_recorder.unarm(zctrl.graph_path[0]) + else: + getattr(self, f'set_{zctrl.symbol}')( + zctrl.graph_path[0], zctrl.value, False) except Exception as e: logging.warning(e) + def add_strip(self, fx_loop=False): + strip = self.lib_zynmixer.addStrip(fx_loop) + if fx_loop: + for proc in self.processors: + if not proc.fx_loop: + self.update_fx_send(proc, strip) + return strip + + def remove_strip(self, chan): + return self.lib_zynmixer.removeStrip(chan) + + def get_path(self, processor): + if processor.chain_id: + if processor.fx_loop: + return f"FX Send {processor.chain_id}" + else: + return f"Mixer Channel {processor.chain_id}" + return f"Main Mixbus" + # Function to set fader level for a channel # channel: Index of channel # level: Fader value (0..+1) @@ -197,14 +284,20 @@ def send_controller_value(self, zctrl): def set_level(self, channel, level, update=True): if channel is None: return - if channel >= self.MAX_NUM_CHANNELS: - channel = self.MAX_NUM_CHANNELS - 1 self.lib_zynmixer.setLevel(channel, ctypes.c_float(level)) if update: self.zctrls[channel]['level'].set_value(level, False) zynsigman.send(zynsigman.S_AUDIO_MIXER, self.SS_ZCTRL_SET_VALUE, chan=channel, symbol="level", value=level) + # Function to get fader level for a channel + # channel: Index of channel + # returns: Fader level (0..+1) + def get_level(self, channel): + if channel is None: + return + return self.lib_zynmixer.getLevel(channel) + # Function to set balance for a channel # channel: Index of channel # balance: Balance value (-1..+1) @@ -212,32 +305,18 @@ def set_level(self, channel, level, update=True): def set_balance(self, channel, balance, update=True): if channel is None: return - if channel >= self.MAX_NUM_CHANNELS: - channel = self.MAX_NUM_CHANNELS - 1 self.lib_zynmixer.setBalance(channel, ctypes.c_float(balance)) if update: self.zctrls[channel]['balance'].set_value(balance, False) zynsigman.send(zynsigman.S_AUDIO_MIXER, self.SS_ZCTRL_SET_VALUE, chan=channel, symbol="balance", value=balance) - # Function to get fader level for a channel - # channel: Index of channel - # returns: Fader level (0..+1) - def get_level(self, channel): - if channel is None: - return - if channel >= self.MAX_NUM_CHANNELS: - channel = self.MAX_NUM_CHANNELS - 1 - return self.lib_zynmixer.getLevel(channel) - # Function to get balance for a channel # channel: Index of channel # returns: Balance value (-1..+1) def get_balance(self, channel): if channel is None: return - if channel >= self.MAX_NUM_CHANNELS: - channel = self.MAX_NUM_CHANNELS - 1 return self.lib_zynmixer.getBalance(channel) # Function to set mute for a channel @@ -247,8 +326,6 @@ def get_balance(self, channel): def set_mute(self, channel, mute, update=False): if channel is None: return - if channel >= self.MAX_NUM_CHANNELS: - channel = self.MAX_NUM_CHANNELS - 1 self.lib_zynmixer.setMute(channel, mute) if update: self.zctrls[channel]['mute'].set_value(mute, False) @@ -261,16 +338,12 @@ def set_mute(self, channel, mute, update=False): def get_mute(self, channel, update=False): if channel is None: return - if channel >= self.MAX_NUM_CHANNELS: - channel = self.MAX_NUM_CHANNELS - 1 return self.lib_zynmixer.getMute(channel) # Function to toggle mute of a channel # channel: Index of channel # update: True for update controller def toggle_mute(self, channel, update=False): - if channel >= self.MAX_NUM_CHANNELS: - channel = self.MAX_NUM_CHANNELS - 1 self.lib_zynmixer.toggleMute(channel) mute = self.lib_zynmixer.getMute(channel) if update: @@ -285,8 +358,6 @@ def toggle_mute(self, channel, update=False): def set_phase(self, channel, phase, update=True): if channel is None: return - if channel >= self.MAX_NUM_CHANNELS: - channel = self.MAX_NUM_CHANNELS - 1 self.lib_zynmixer.setPhase(channel, phase) if update: self.zctrls[channel]['phase'].set_value(phase, False) @@ -299,8 +370,6 @@ def set_phase(self, channel, phase, update=True): def get_phase(self, channel): if channel is None: return - if channel >= self.MAX_NUM_CHANNELS: - channel = self.MAX_NUM_CHANNELS - 1 return self.lib_zynmixer.getPhase(channel) # Function to toggle phase reversal of a channel @@ -309,8 +378,6 @@ def get_phase(self, channel): def toggle_phase(self, channel, update=True): if channel is None: return - if channel >= self.MAX_NUM_CHANNELS: - channel = self.MAX_NUM_CHANNELS - 1 self.lib_zynmixer.togglePhase(channel) phase = self.lib_zynmixer.getPhase(channel) if update: @@ -325,20 +392,19 @@ def toggle_phase(self, channel, update=True): def set_solo(self, channel, solo, update=True): if channel is None: return - if channel >= self.MAX_NUM_CHANNELS: - channel = self.MAX_NUM_CHANNELS - 1 self.lib_zynmixer.setSolo(channel, solo) if update: - self.zctrls[channel]['solo'].set_value(solo, False) - zynsigman.send(zynsigman.S_AUDIO_MIXER, self.SS_ZCTRL_SET_VALUE, + #TODO: Get correct channel/processor + self.processors[channel].controllers_dict['solo'].set_value(solo, False) + if channel: + zynsigman.send(zynsigman.S_AUDIO_MIXER, self.SS_ZCTRL_SET_VALUE, chan=channel, symbol="solo", value=solo) - - if channel == self.MAX_NUM_CHANNELS - 1: + else: # Main strip solo clears all chain solo - for i in range(0, self.MAX_NUM_CHANNELS - 2): - self.zctrls[i]['solo'].set_value(solo, 0) + for proc in self.processors: + proc.controllers_dict["solo"].set_value(0, False) zynsigman.send( - zynsigman.S_AUDIO_MIXER, self.SS_ZCTRL_SET_VALUE, chan=i, symbol="solo", value=0) + zynsigman.S_AUDIO_MIXER, self.SS_ZCTRL_SET_VALUE, chan=proc.mixer_chan, symbol="solo", value=0) # Function to get solo for a channel # channel: Index of channel @@ -346,8 +412,6 @@ def set_solo(self, channel, solo, update=True): def get_solo(self, channel): if channel is None: return - if channel >= self.MAX_NUM_CHANNELS: - channel = self.MAX_NUM_CHANNELS - 1 return self.lib_zynmixer.getSolo(channel) == 1 # Function to toggle mute of a channel @@ -356,8 +420,6 @@ def get_solo(self, channel): def toggle_solo(self, channel, update=True): if channel is None: return - if channel >= self.MAX_NUM_CHANNELS: - channel = self.MAX_NUM_CHANNELS - 1 if self.get_solo(channel): self.set_solo(channel, False) else: @@ -370,8 +432,6 @@ def toggle_solo(self, channel, update=True): def set_mono(self, channel, mono, update=True): if channel is None: return - if channel >= self.MAX_NUM_CHANNELS: - channel = self.MAX_NUM_CHANNELS - 1 self.lib_zynmixer.setMono(channel, mono) if update: self.zctrls[channel]['mono'].set_value(mono, False) @@ -384,8 +444,6 @@ def set_mono(self, channel, mono, update=True): def get_mono(self, channel): if channel is None: return - if channel >= self.MAX_NUM_CHANNELS: - channel = self.MAX_NUM_CHANNELS - 1 return self.lib_zynmixer.getMono(channel) == 1 # Function to get all mono @@ -404,8 +462,6 @@ def get_all_monos(self): def toggle_mono(self, channel, update=True): if channel is None: return - if channel >= self.MAX_NUM_CHANNELS: - channel = self.MAX_NUM_CHANNELS - 1 if self.get_mono(channel): self.set_mono(channel, False) else: @@ -422,8 +478,6 @@ def toggle_mono(self, channel, update=True): def set_ms(self, channel, enable, update=True): if channel is None: return - if channel >= self.MAX_NUM_CHANNELS: - channel = self.MAX_NUM_CHANNELS - 1 self.lib_zynmixer.setMS(channel, enable) if update: self.zctrls[channel]['ms'].set_value(enable, False) @@ -436,8 +490,6 @@ def set_ms(self, channel, enable, update=True): def get_ms(self, channel): if channel is None: return - if channel >= self.MAX_NUM_CHANNELS: - channel = self.MAX_NUM_CHANNELS - 1 return self.lib_zynmixer.getMS(channel) == 1 # Function to toggle M+S mode @@ -446,8 +498,6 @@ def get_ms(self, channel): def toggle_ms(self, channel, update=True): if channel is None: return - if channel >= self.MAX_NUM_CHANNELS: - channel = self.MAX_NUM_CHANNELS - 1 if self.get_ms(channel): self.set_ms(channel, False) else: @@ -456,13 +506,37 @@ def toggle_ms(self, channel, update=True): self.zctrls[channel]['ms'].set_value( self.lib_zynmixer.getMS(channel), False) + # Function to set fx send level for a channel + # channel: Index of channel + # send: Index of fx send + # level: Fader value (0..+1) + # update: True for update controller + + def set_send(self, channel, send, level, update=True): + if channel is None or send is None: + return + self.lib_zynmixer.setSend(channel, send, ctypes.c_float(level)) + if update: + self.zctrls[channel][f'send_{send:02d}'].set_value(level, False) + zynsigman.send(zynsigman.S_AUDIO_MIXER, self.SS_ZCTRL_SET_VALUE, + chan=channel, symbol=f"send_{send:02d}", value=level) + + # Function to get fx send level for a channel + # channel: Index of channel + # send: Index of fx send + # returns: Send level (0..+1) + def get_send(self, channel, send): + if channel is None or send is None: + return + return self.lib_zynmixer.getSend(channel, send) + # Function to set internal normalisation of a channel when its direct output is not routed # channel: Index of channel # enable: True to enable internal normalisation def normalise(self, channel, enable): if channel is None: return - if channel >= self.MAX_NUM_CHANNELS - 1: + if channel == 0: return # Don't allow normalisation of main mixbus (to itself) self.lib_zynmixer.setNormalise(channel, enable) @@ -473,30 +547,8 @@ def normalise(self, channel, enable): def is_normalised(self, channel): if channel is None: return False - if channel >= self.MAX_NUM_CHANNELS: - channel = self.MAX_NUM_CHANNELS - 1 return self.lib_zynmixer.getNormalise(channel) - # Function to check if channel has audio routed to its input - # channel: Index of channel - # returns: True if routed - def is_channel_routed(self, channel): - if channel is None: - return - if channel >= self.MAX_NUM_CHANNELS: - channel = self.MAX_NUM_CHANNELS - 1 - return (self.lib_zynmixer.isChannelRouted(channel) != 0) - - # Function to check if channel output is routed - # channel: Index of channel - # returns: True if routed - def is_channel_out_routed(self, channel): - if channel is None: - return - if channel >= self.MAX_NUM_CHANNELS: - channel = self.MAX_NUM_CHANNELS - 1 - return (self.lib_zynmixer.isChannelOutRouted(channel) != 0) - # Function to get peak programme level for a channel # channel: Index of channel # leg: 0 for A-leg (left), 1 for B-leg (right) @@ -504,8 +556,6 @@ def is_channel_out_routed(self, channel): def get_dpm(self, channel, leg): if channel is None: return - if channel >= self.MAX_NUM_CHANNELS: - channel = self.MAX_NUM_CHANNELS - 1 return self.lib_zynmixer.getDpm(channel, leg) # Function to get peak programme hold level for a channel @@ -515,8 +565,6 @@ def get_dpm(self, channel, leg): def get_dpm_holds(self, channel, leg): if channel is None: return - if channel >= self.MAX_NUM_CHANNELS: - channel = self.MAX_NUM_CHANNELS - 1 return self.lib_zynmixer.getDpmHold(channel, leg) # Function to get the dpm states for a set of channels @@ -558,133 +606,8 @@ def remove_osc_client(self, client): self.lib_zynmixer.removeOscClient( ctypes.c_char_p(client.encode('utf-8'))) - # -------------------------------------------------------------------------- - # State management (for snapshots) - # -------------------------------------------------------------------------- - - def reset(self, strip): - """Reset mixer strip to default values - - strip : Index of mixer strip - """ - if not isinstance(strip, int): - return - if strip >= self.MAX_NUM_CHANNELS: - strip = self.MAX_NUM_CHANNELS - 1 - self.zctrls[strip]['level'].reset_value() - self.zctrls[strip]['balance'].reset_value() - self.zctrls[strip]['mute'].reset_value() - self.zctrls[strip]['mono'].reset_value() - self.zctrls[strip]['solo'].reset_value() - self.zctrls[strip]['phase'].reset_value() - # for symbol in self.zctrls[strip]: - # self.zctrls[strip][symbol].midi_unlearn() - - # Reset mixer to default state - def reset_state(self): + def reset(self): for channel in range(self.MAX_NUM_CHANNELS): - self.reset(channel) - - def get_state(self, full=True): - """Get mixer state as list of controller state dictionaries - - full : True to get state of all parameters or false for off-default values - Returns : List of dictionaries describing parameter states - """ - state = {} - for chan in range(self.MAX_NUM_CHANNELS): - key = 'chan_{:02d}'.format(chan) - chan_state = {} - for symbol in self.zctrls[chan]: - zctrl = self.zctrls[chan][symbol] - value = zctrl.value - if zctrl.is_toggle: - value |= (zctrl.midi_cc_momentary_switch << 1) - if value != zctrl.value_default: - chan_state[zctrl.symbol] = value - if chan_state: - state[key] = chan_state - state["midi_learn"] = {} - for chan in range(16): - for cc, zctrl in self.learned_cc[chan].items(): - state["midi_learn"][f"{chan},{cc}"] = zctrl.graph_path - return state - - def set_state(self, state, full=True): - """Set mixer state - - state : List of mixer channels containing dictionary of each state value - full : True to reset parameters omitted from state - """ - - for chan, zctrls in enumerate(self.zctrls): - key = 'chan_{:02d}'.format(chan) - for symbol, zctrl in zctrls.items(): - try: - if zctrl.is_toggle: - zctrl.set_value(state[key][symbol] & 1, True) - zctrl.midi_cc_momentary_switch = state[key][symbol] >> 1 - else: - zctrl.set_value(state[key][symbol], True) - except: - if full: - zctrl.reset_value() - if "midi_learn" in state: - # state["midi_learn"][f"{chan},{cc}"] = zctrl.graph_path - self.midi_unlearn_all() - for ml, graph_path in state["midi_learn"].items(): - try: - chan, cc = ml.split(',') - zctrl = self.zctrls[graph_path[0]][graph_path[1]] - self.learned_cc[int(chan)][int(cc)] = zctrl - except Exception as e: - logging.warning( - f"Failed to restore mixer midi learn: {ml} => {graph_path} ({e})") - - # -------------------------------------------------------------------------- - # MIDI Learn - # -------------------------------------------------------------------------- - - def midi_control_change(self, chan, ccnum, val): - if self.midi_learn_zctrl and self.midi_learn_zctrl != True: - for midi_chan in range(16): - for midi_cc in self.learned_cc[midi_chan]: - if self.learned_cc[midi_chan][midi_cc] == self.midi_learn_zctrl: - self.learned_cc[midi_chan].pop(midi_cc) - break - self.learned_cc[chan][ccnum] = self.midi_learn_zctrl - self.disable_midi_learn() - if self.midi_learn_cb: - self.midi_learn_cb() - else: - for ch in range(16): - try: - self.learned_cc[ch][ccnum].midi_control_change(val) - break - except: - pass - - def midi_unlearn(self, zctrl): - for chan, learned in enumerate(self.learned_cc): - for cc, ctrl in learned.items(): - if ctrl == zctrl: - self.learned_cc[chan].pop(cc) - return - - def midi_unlearn_chan(self, chan): - for zctrl in self.zctrls[chan].values(): - self.midi_unlearn(zctrl) - - def midi_unlearn_all(self, not_used=None): - self.learned_cc = [dict() for x in range(16)] - - def enable_midi_learn(self, zctrl): - self.midi_learn_zctrl = zctrl - - def disable_midi_learn(self): - self.midi_learn_zctrl = None - - def set_midi_learn_cb(self, cb): - self.midi_learn_cb = cb + self.lib_zynmixer.reset(channel) # ------------------------------------------------------------------------------- diff --git a/zyngine/zynthian_engine_inet_radio.py b/zyngine/zynthian_engine_inet_radio.py index 1156e4474..5db8c0a7f 100644 --- a/zyngine/zynthian_engine_inet_radio.py +++ b/zyngine/zynthian_engine_inet_radio.py @@ -49,8 +49,8 @@ class zynthian_engine_inet_radio(zynthian_engine): # Initialization # --------------------------------------------------------------------------- - def __init__(self, zyngui=None): - super().__init__(zyngui) + def __init__(self, state_manager=None): + super().__init__(state_manager) self.name = "InternetRadio" self.nickname = "IR" self.jackname = "inetradio" diff --git a/zyngine/zynthian_engine_setbfree.py b/zyngine/zynthian_engine_setbfree.py index 5ba217605..60a62a6d5 100644 --- a/zyngine/zynthian_engine_setbfree.py +++ b/zyngine/zynthian_engine_setbfree.py @@ -315,7 +315,6 @@ def start(self): self.processors[i].get_bank_list() self.processors[i].set_bank(0, False) chain.audio_out = [] - chain.mixer_chan = None except Exception as e: logging.error( "%s Manual processor can't be added! => %s", manual, e) @@ -326,7 +325,6 @@ def start(self): self.set_midi_chan(self.processors[i]) self.processors[i].refresh_controllers() chain.audio_out = [] - chain.mixer_chan = None if self.manuals_split_config: # Enable Active MIDI Channel for splitted configurations @@ -449,9 +447,8 @@ def add_processor(self, processor): return # Disable mixer strip for extra manuals - if n > 0: - self.state_manager.chain_manager.get_chain( - processor.chain_id).mixer_chan = None + if n == 0: + self.state_manager.chain_manager.add_processor(processor.chain_id, "AM") self.set_midi_chan(processor) super().add_processor(processor) diff --git a/zyngine/zynthian_lv2.py b/zyngine/zynthian_lv2.py index 456df8e0d..0a43ae84f 100755 --- a/zyngine/zynthian_lv2.py +++ b/zyngine/zynthian_lv2.py @@ -165,7 +165,8 @@ class EngineType(Enum): 'MC': ["MIDI Control", "MIDI Control External", "MIDI Tool", "Other", True], 'IR': ["InternetRadio", "Internet Radio", "Audio Generator", "Other", True], 'PD': ["PureData", "PureData - Visual Programming", "Special", "Language", True], - 'MD': ["MOD-UI", "MOD-UI - Plugin Host", "Special", "Language", True] + 'MD': ["MOD-UI", "MOD-UI - Plugin Host", "Special", "Language", True], + 'AM': ["AudioMixer", "Audio Mixer Strip", "Audio Effect", "Other", True] } ENGINE_DEFAULT_CONFIG_FILE = "{}/config/engine_config.json".format( diff --git a/zyngine/zynthian_processor.py b/zyngine/zynthian_processor.py index a6d9dd271..364be1e9e 100644 --- a/zyngine/zynthian_processor.py +++ b/zyngine/zynthian_processor.py @@ -67,6 +67,7 @@ def __init__(self, eng_code, eng_info, id=None): self.jackname = None self.chain = None self.chain_id = None + self.bypass = False self.bank_list = [] self.bank_index = 0 diff --git a/zyngine/zynthian_state_manager.py b/zyngine/zynthian_state_manager.py index f757c37e8..53f33972b 100644 --- a/zyngine/zynthian_state_manager.py +++ b/zyngine/zynthian_state_manager.py @@ -147,7 +147,6 @@ def __init__(self): self.hwmon_thermal_file = None self.hwmon_undervolt_file = None - self.zynmixer = zynthian_engine_audio_mixer.zynmixer() self.chain_manager = zynthian_chain_manager(self) self.reset_zs3() @@ -231,12 +230,11 @@ def start(self): zynautoconnect.start(self) self.jack_period = self.get_jackd_blocksize() / self.get_jackd_samplerate() - self.zynmixer.reset_state() self.ctrldev_manager = zynthian_ctrldev_manager(self) self.reload_midi_config() self.create_audio_player() self.chain_manager.add_chain(0) - + self.zynmixer = self.chain_manager.add_processor(0, "AM", eng_config={"fx_loop":True}).engine self.exit_flag = False self.slow_thread = Thread(target=self.slow_thread_task) self.slow_thread.name = "Status Manager Slow" @@ -307,7 +305,7 @@ def clean(self, chains=True, zynseq=True): sequences : True for cleaning zynseq state (sequences) """ - self.zynmixer.set_mute(self.zynmixer.MAX_NUM_CHANNELS - 1, 1) + self.zynmixer.set_mute(0, 1) # self.zynseq.transport_stop("ALL") self.zynseq.libseq.stop() if zynseq: @@ -316,12 +314,12 @@ def clean(self, chains=True, zynseq=True): zynautoconnect.pause() self.chain_manager.remove_all_chains(True) self.reset_zs3() - self.zynmixer.reset_state() + self.zynmixer.reset() self.reload_midi_config() zynautoconnect.request_midi_connect(True) zynautoconnect.request_audio_connect(True) zynautoconnect.resume() - self.zynmixer.set_mute(self.zynmixer.MAX_NUM_CHANNELS - 1, 0) + self.zynmixer.set_mute(0, 0) def clean_all(self): """Remove ALL Chains & Sequences.""" @@ -811,9 +809,6 @@ def zynmidi_read(self): if self.midi_learn_zctrl: self.chain_manager.add_midi_learn( chan, ccnum, self.midi_learn_zctrl, izmip) - else: - self.zynmixer.midi_control_change( - chan, ccnum, ccval) # Master Note CUIA with ZynSwitch emulation elif evtype == 0x8 or evtype == 0x9: note = str(ev[1] & 0x7F) @@ -846,8 +841,6 @@ def zynmidi_read(self): if not self.midi_learn_zctrl: self.chain_manager.midi_control_change( izmip, chan, ccnum, ccval) - self.zynmixer.midi_control_change( - chan, ccnum, ccval) self.alsa_mixer_processor.midi_control_change( chan, ccnum, ccval) self.audio_player.midi_control_change( @@ -973,14 +966,6 @@ def get_state(self): if zynthian_gui_config.snapshot_mixer_settings and self.alsa_mixer_processor: state['alsa_mixer'] = self.alsa_mixer_processor.get_state() - # Audio Recorder Armed - armed_state = [] - for midi_chan in range(self.zynmixer.MAX_NUM_CHANNELS): - if self.audio_recorder.is_armed(midi_chan): - armed_state.append(midi_chan) - if armed_state: - state['audio_recorder_armed'] = armed_state - # Zynseq RIFF data binary_riff_data = self.zynseq.get_riff_data() b64_data = base64.b64encode(binary_riff_data) @@ -1103,15 +1088,14 @@ def load_snapshot(self, fpath, load_chains=True, load_sequences=True, merge=Fals self.end_busy("load snapshot") return None - mute = self.zynmixer.get_mute(self.zynmixer.MAX_NUM_CHANNELS - 1) + mute = self.zynmixer.get_mute(0) try: snapshot = JSONDecoder().decode(json) state = self.fix_snapshot(snapshot) if load_chains: # Mute output to avoid unwanted noises - self.zynmixer.set_mute( - self.zynmixer.MAX_NUM_CHANNELS - 1, True) + self.zynmixer.set_mute(0, True) zynautoconnect.pause() if "chains" in state: @@ -1141,13 +1125,8 @@ def load_snapshot(self, fpath, load_chains=True, load_sequences=True, merge=Fals if new_proc_id <= id: new_proc_id = id + 1 - mixer_chan = 0 for chain_id, chain_state in state["chains"].items(): # Fix mixer channel - mixer_chan = self.chain_manager.get_next_free_mixer_chan(mixer_chan) - mixer_map[int(chain_state["mixer_chan"])] = mixer_chan - chain_state["mixer_chan"] = mixer_chan - mixer_chan += 1 new_chain_id = 1 while new_chain_id in self.chain_manager.chains: new_chain_id += 1 @@ -1212,13 +1191,6 @@ def load_snapshot(self, fpath, load_chains=True, load_sequences=True, merge=Fals if "alsa_mixer" in state: self.alsa_mixer_processor.set_state(state["alsa_mixer"]) - if "audio_recorder_armed" in state: - for midi_chan in range(self.zynmixer.MAX_NUM_CHANNELS): - if midi_chan in state["audio_recorder_armed"]: - self.audio_recorder.arm(midi_chan) - else: - self.audio_recorder.unarm(midi_chan) - if "midi_profile_state" in state: self.set_midi_profile_state(state["midi_profile_state"]) @@ -1248,7 +1220,7 @@ def load_snapshot(self, fpath, load_chains=True, load_sequences=True, merge=Fals zynautoconnect.request_audio_connect(True) # Restore mute state - self.zynmixer.set_mute(self.zynmixer.MAX_NUM_CHANNELS - 1, mute) + self.zynmixer.set_mute(0, mute) # Signal snapshot loading zynsigman.send_queued(zynsigman.S_STATE_MAN, self.SS_LOAD_SNAPSHOT) @@ -1414,14 +1386,6 @@ def load_zs3(self, zs3_id): else: continue - try: - if zs3_state["mixer"][f"chan_{chain.mixer_chan:02}"]["mute"]: - # Avoid subsequent config changes from being heard on muted chains - self.zynmixer.set_mute(chain.mixer_chan, 1) - mute_pause = True - except: - pass - if "midi_chan" in chain_state: if chain.midi_chan is not None and chain.midi_chan != chain_state['midi_chan']: self.chain_manager.set_midi_chan( @@ -1506,15 +1470,6 @@ def load_zs3(self, zs3_id): self.chain_manager.set_active_chain_by_id( zs3_state["active_chain"]) - if "mixer" in zs3_state: - try: - restore_flag = zs3_state["mixer"]["restore"] - except: - restore_flag = True - if restore_flag: - self.set_busy_details("restoring mixer state") - self.zynmixer.set_state(zs3_state["mixer"]) - if "midi_capture" in zs3_state: self.set_busy_details("restoring midi capture state") self.set_midi_capture_state(zs3_state['midi_capture']) @@ -1662,11 +1617,6 @@ def save_zs3(self, zs3_id=None, title=None): if processor_states: self.zs3[zs3_id]["processors"] = processor_states - # Add mixer state - mixer_state = self.zynmixer.get_state(False) - if mixer_state: - self.zs3[zs3_id]["mixer"] = mixer_state - # Add MIDI capture state mcstate = self.get_midi_capture_state() if mcstate: diff --git a/zyngui/zynthian_gui.py b/zyngui/zynthian_gui.py index 0d434d939..b629cb3d6 100644 --- a/zyngui/zynthian_gui.py +++ b/zyngui/zynthian_gui.py @@ -154,7 +154,7 @@ def __init__(self): self.status_counter = 0 self.modify_chain_status = { - "midi_thru": False, "audio_thru": False, "parallel": False} + "midi_thru": False, "audio_thru": False} self.capture_log_ts0 = None self.capture_log_fname = None @@ -410,7 +410,7 @@ def osc_cb_all(self, path, args, types, src): return self.osc_clients[src.hostname] = monotonic() self.state_manager.zynmixer.enable_dpm( - 0, self.state_manager.zynmixer.MAX_NUM_CHANNELS - 2, True) + 1, self.state_manager.zynmixer.MAX_NUM_CHANNELS - 1, True) else: if part2[:6] == "VOLUME": self.state_manager.zynmixer.set_level( @@ -879,13 +879,10 @@ def modify_chain(self, status=None): self.modify_chain_status["chain_id"], processor, force_bank_preset=True) else: # Adding processor to existing chain - parallel = "parallel" in self.modify_chain_status and self.modify_chain_status[ - "parallel"] - post_fader = "post_fader" in self.modify_chain_status and self.modify_chain_status[ - "post_fader"] processor = self.chain_manager.add_processor( - self.modify_chain_status["chain_id"], self.modify_chain_status["engine"], parallel=parallel, post_fader=post_fader) + self.modify_chain_status["chain_id"], self.modify_chain_status["engine"]) if processor: + zynautoconnect.autoconnect() self.close_screen("loading") self.chain_control( self.modify_chain_status["chain_id"], processor, force_bank_preset=True) @@ -899,6 +896,8 @@ def modify_chain(self, status=None): self.modify_chain_status["midi_thru"] = False if "audio_thru" not in self.modify_chain_status: self.modify_chain_status["audio_thru"] = False + if "fx_loop" not in self.modify_chain_status: + self.modify_chain_status["fx_loop"] = False # Detect MOD-UI special chain and assign dedicated zmop index if self.modify_chain_status["engine"] == "MD": zmop_index = ZMOP_MOD_INDEX @@ -909,7 +908,8 @@ def modify_chain(self, status=None): self.modify_chain_status["midi_chan"], self.modify_chain_status["midi_thru"], self.modify_chain_status["audio_thru"], - zmop_index=zmop_index + self.modify_chain_status["fx_loop"], + zmop_index ) if chain_id is None: self.show_screen_reset("audio_mixer") @@ -919,7 +919,13 @@ def modify_chain(self, status=None): chain_id, self.modify_chain_status["engine"] ) - # self.modify_chain_status = {"midi_thru": False, "audio_thru": False, "parallel": False} + if self.chain_manager.chains[chain_id].is_audio() and self.chain_manager.chains[chain_id].get_slot_count("Audio Effect") == 0: + am_proc = self.chain_manager.add_processor(chain_id, "AM", eng_config={"fx_loop": self.modify_chain_status["fx_loop"]}) + if self.modify_chain_status['fx_loop']: + self.chain_manager.chains[chain_id].set_title(f"FX Return {am_proc.mixer_chan}") + zynautoconnect.request_audio_connect(True) + zynautoconnect.request_midi_connect(True) + # self.modify_chain_status = {"midi_thru": False, "audio_thru": False} if processor: self.close_screen("loading") self.chain_control( @@ -2604,7 +2610,7 @@ def osc_timeout(self): if not self.osc_clients and self.current_screen != "audio_mixer": self.state_manager.zynmixer.enable_dpm( - 0, self.state_manager.zynmixer.MAX_NUM_CHANNELS - 2, False) + 1, self.state_manager.zynmixer.MAX_NUM_CHANNELS - 1, False) # Poll zynthian_gui_config.top.after( diff --git a/zyngui/zynthian_gui_audio_out.py b/zyngui/zynthian_gui_audio_out.py index 85e38c671..8dfa1dc59 100644 --- a/zyngui/zynthian_gui_audio_out.py +++ b/zyngui/zynthian_gui_audio_out.py @@ -27,11 +27,7 @@ # Zynthian specific modules import zynautoconnect -from zyngine.zynthian_signal_manager import zynsigman -from zyngui import zynthian_gui_config from zyngui.zynthian_gui_selector import zynthian_gui_selector -from zyngine.zynthian_engine_modui import zynthian_engine_modui -from zyngine.zynthian_audio_recorder import zynthian_audio_recorder # ------------------------------------------------------------------------------ # Zynthian Audio-Out Selection GUI Class @@ -44,24 +40,6 @@ def __init__(self): self.chain = None super().__init__('Audio Out', True) - def build_view(self): - if super().build_view(): - zynsigman.register_queued( - zynsigman.S_AUDIO_RECORDER, zynthian_audio_recorder.SS_AUDIO_RECORDER_STATE, self.update_rec) - return True - else: - return False - - def hide(self): - if self.shown: - zynsigman.unregister( - zynsigman.S_AUDIO_RECORDER, zynthian_audio_recorder.SS_AUDIO_RECORDER_STATE, self.update_rec) - super().hide() - - def update_rec(self, state): - # Lock multitrack record config when recorder is recording - self.fill_list() - def set_chain(self, chain): self.chain = chain @@ -118,31 +96,13 @@ def fill_list(self): self.list_data.append( (processor, processor, "\u2610 " + title)) - self.list_data.append((None, None, "> Audio Recorder")) - armed = self.zyngui.state_manager.audio_recorder.is_armed( - self.chain.mixer_chan) - if self.zyngui.state_manager.audio_recorder.status: - locked = None - else: - locked = "record" - if armed: - self.list_data.append( - (locked, 'record_disable', '\u2612 Record chain')) - else: - self.list_data.append( - (locked, 'record_enable', '\u2610 Record chain')) - super().fill_list() def fill_listbox(self): super().fill_listbox() def select_action(self, i, t='S'): - if self.list_data[i][0] == 'record': - self.zyngui.state_manager.audio_recorder.toggle_arm( - self.chain.mixer_chan) - else: - self.chain.toggle_audio_out(self.list_data[i][0]) + self.chain.toggle_audio_out(self.list_data[i][0]) self.fill_list() def set_select_path(self): diff --git a/zyngui/zynthian_gui_base.py b/zyngui/zynthian_gui_base.py index 2fdeae0a3..cf6a9389b 100644 --- a/zyngui/zynthian_gui_base.py +++ b/zyngui/zynthian_gui_base.py @@ -638,15 +638,12 @@ def init_status(self): def init_dpmeter(self): width = int(self.status_l - 2 * self.status_rh - 1) height = int(self.status_h / 4 - 2) - self.dpm_a = zynthian_gui_dpm(self.zyngui.state_manager.zynmixer, self.zyngui.state_manager.zynmixer.MAX_NUM_CHANNELS - - 1, 0, self.status_canvas, 0, 0, width, height, False, ("status_dpm")) - self.dpm_b = zynthian_gui_dpm(self.zyngui.state_manager.zynmixer, self.zyngui.state_manager.zynmixer.MAX_NUM_CHANNELS - - 1, 1, self.status_canvas, 0, height + 2, width, height, False, ("status_dpm")) + self.dpm_a = zynthian_gui_dpm(self.zyngui.state_manager.zynmixer, 0, 0, self.status_canvas, 0, 0, width, height, False, ("status_dpm")) + self.dpm_b = zynthian_gui_dpm(self.zyngui.state_manager.zynmixer, 0, 1, self.status_canvas, 0, height + 2, width, height, False, ("status_dpm")) def refresh_status(self): if self.shown: - mute = self.zyngui.state_manager.zynmixer.get_mute( - self.zyngui.state_manager.zynmixer.MAX_NUM_CHANNELS - 1) + mute = self.zyngui.state_manager.zynmixer.get_mute(0) if True: # mute != self.main_mute: self.main_mute = mute if mute: @@ -662,8 +659,7 @@ def refresh_status(self): self.status_canvas.itemconfigure( 'status_dpm', state=tkinter.NORMAL) if not mute and self.dpm_a: - state = self.zyngui.state_manager.zynmixer.get_dpm_states( - self.zyngui.state_manager.zynmixer.MAX_NUM_CHANNELS - 1, self.zyngui.state_manager.zynmixer.MAX_NUM_CHANNELS - 1)[0] + state = self.zyngui.state_manager.zynmixer.get_dpm_states(0, 0)[0] self.dpm_a.refresh(state[0], state[2], state[4]) self.dpm_b.refresh(state[1], state[3], state[4]) diff --git a/zyngui/zynthian_gui_chain_menu.py b/zyngui/zynthian_gui_chain_menu.py index c0ac75244..c1895b574 100644 --- a/zyngui/zynthian_gui_chain_menu.py +++ b/zyngui/zynthian_gui_chain_menu.py @@ -42,23 +42,16 @@ def __init__(self): def fill_list(self): self.list_data = [] - try: - self.zyngui.chain_manager.get_next_free_mixer_chan() - mixer_avail = True - except: - mixer_avail = False self.list_data.append((None, 0, "> ADD CHAIN")) - if mixer_avail: - self.list_data.append( - (self.add_synth_chain, 0, "Add Instrument Chain")) - self.list_data.append((self.add_audiofx_chain, 0, "Add Audio Chain")) + self.list_data.append( + (self.add_synth_chain, 0, "Add Instrument Chain")) + self.list_data.append((self.add_audiofx_chain, 0, "Add Audio Chain")) self.list_data.append((self.add_midifx_chain, 0, "Add MIDI Chain")) - if mixer_avail: - self.list_data.append( - (self.add_midiaudiofx_chain, 0, "Add MIDI+Audio Chain")) - self.list_data.append( - (self.add_generator_chain, 0, "Add Audio Generator Chain")) - self.list_data.append((self.add_special_chain, 0, "Add Special Chain")) + self.list_data.append( + (self.add_midiaudiofx_chain, 0, "Add MIDI+Audio Chain")) + self.list_data.append( + (self.add_generator_chain, 0, "Add Audio Generator Chain")) + self.list_data.append((self.add_special_chain, 0, "Add Special Chain")) self.list_data.append((None, 0, "> REMOVE")) self.list_data.append((self.remove_sequences, 0, "Remove Sequences")) diff --git a/zyngui/zynthian_gui_chain_options.py b/zyngui/zynthian_gui_chain_options.py index edd851df4..db3b88791 100644 --- a/zyngui/zynthian_gui_chain_options.py +++ b/zyngui/zynthian_gui_chain_options.py @@ -42,13 +42,11 @@ def __init__(self): self.index = 0 self.chain = None self.chain_id = None - self.processor = None - def setup(self, chain_id=None, proc=None): + def setup(self, chain_id=None): self.index = 0 self.chain = self.zyngui.chain_manager.get_chain(chain_id) self.chain_id = self.chain.chain_id - self.processor = proc def fill_list(self): self.list_data = [] @@ -78,18 +76,15 @@ def fill_list(self): if self.chain.get_processor_count() and not zynthian_gui_config.check_wiring_layout(["Z2", "V5"]): # TODO Disable midi learn for some chains??? - self.list_data.append((self.midi_learn, None, "MIDI Learn")) + self.list_data.append((self.clean_midi_learn, None, "Clean MIDI Learn")) - if self.chain.audio_thru and self.chain_id != 0: + if self.chain.audio_thru and self.chain_id != 0 and not self.chain.fx_loop: self.list_data.append((self.chain_audio_capture, None, "Audio In")) if self.chain.is_audio(): self.list_data.append( (self.chain_audio_routing, None, "Audio Out")) - if self.chain.is_audio(): - self.list_data.append((self.audio_options, None, "Audio Options")) - # TODO: Catch signal for Audio Recording status change if self.chain_id == 0 and not zynthian_gui_config.check_wiring_layout(["Z2", "V5"]): if self.zyngui.state_manager.audio_recorder.status: @@ -110,18 +105,16 @@ def fill_list(self): if self.chain.is_audio(): # Add Audio-FX options self.list_data.append( - (self.audiofx_add, None, "Add Pre-fader Audio-FX")) - self.list_data.append( - (self.postfader_add, None, "Add Post-fader Audio-FX")) + (self.audiofx_add, None, "Add Audio-FX")) if self.chain_id != 0: - if synth_proc_count * midi_proc_count + audio_proc_count == 0: + if synth_proc_count * midi_proc_count == 0 and audio_proc_count < 2: self.list_data.append( (self.remove_chain, None, "Remove Chain")) else: self.list_data.append((self.remove_cb, None, "Remove...")) self.list_data.append((self.export_chain, None, "Export chain as snapshot...")) - elif audio_proc_count > 0: + elif audio_proc_count > 1: self.list_data.append( (self.remove_all_audiofx, None, "Remove all Audio-FX")) @@ -158,26 +151,7 @@ def generate_chaintree_menu(self): indent + "╰━ " + proc.get_name())) indent += 1 # Build pre-fader audio effects chain - for slot in range(self.chain.fader_pos): - if self.chain.fader_pos <= slot: - break - procs = self.chain.get_processors("Audio Effect", slot) - num_procs = len(procs) - for index, processor in enumerate(procs): - name = processor.get_name() - if index == num_procs - 1: - res.append((self.processor_options, processor, - " " * indent + "┗━ " + name)) - else: - res.append((self.processor_options, processor, - " " * indent + "┣━ " + name)) - indent += 1 - # Add FADER mark - if self.chain.audio_thru or self.chain.synth_slots: - res.append((None, None, " " * indent + "┗━ FADER")) - indent += 1 - # Build post-fader audio effects chain - for slot in range(self.chain.fader_pos, len(self.chain.audio_slots)): + for slot in range(self.chain.get_slot_count("Audio Effect")): procs = self.chain.get_processors("Audio Effect", slot) num_procs = len(procs) for index, processor in enumerate(procs): @@ -247,7 +221,6 @@ def arrow_right(self): # We don't call setup() because it reset the list position (index) self.chain_id = chain_keys[index] self.chain = self.zyngui.chain_manager.get_chain(self.chain_id) - self.processor = None self.set_select_path() self.fill_list() @@ -261,7 +234,6 @@ def arrow_left(self): # We don't call setup() because it reset the list position (index) self.chain_id = chain_keys[index] self.chain = self.zyngui.chain_manager.get_chain(self.chain_id) - self.processor = None self.set_select_path() self.fill_list() @@ -286,31 +258,9 @@ def chain_note_range(self): self.zyngui.screens['midi_key_range'].config(self.chain) self.zyngui.show_screen('midi_key_range') - def midi_learn(self): - options = {} - options['Enter MIDI-learn'] = "enable_midi_learn" - options['Enter Global MIDI-learn'] = "enable_global_midi_learn" - if self.processor: - options[f'Clear {self.processor.name} MIDI-learn'] = "clean_proc" - options['Clear chain MIDI-learn'] = "clean_chain" - self.zyngui.screens['option'].config( - "MIDI-learn", options, self.midi_learn_menu_cb) - self.zyngui.show_screen('option') - - def midi_learn_menu_cb(self, options, params): - if params == 'enable_midi_learn': - self.zyngui.close_screen() - self.zyngui.cuia_toggle_midi_learn() - elif params == 'enable_global_midi_learn': - self.zyngui.close_screen() - self.zyngui.cuia_toggle_midi_learn() - self.zyngui.cuia_toggle_midi_learn() - elif params == 'clean_proc': - self.zyngui.show_confirm( - f"Do you want to clean MIDI-learn for ALL controls in processor {self.processor.name}?", self.zyngui.chain_manager.clean_midi_learn, self.processor) - elif params == 'clean_chain': - self.zyngui.show_confirm( - f"Do you want to clean MIDI-learn for ALL controls in ALL processors within chain {self.chain_id:02d}?", self.zyngui.chain_manager.clean_midi_learn, self.chain_id) + def clean_midi_learn(self): + self.zyngui.show_confirm( + f"Do you want to remove all MIDI CC binding for ALL controls in ALL processors within chain {self.chain_id:02d}?", self.zyngui.chain_manager.clean_midi_learn, self.chain_id) def chain_midi_routing(self): self.zyngui.screens['midi_config'].set_chain(self.chain) @@ -321,36 +271,6 @@ def chain_audio_routing(self): self.zyngui.screens['audio_out'].set_chain(self.chain) self.zyngui.show_screen('audio_out') - def audio_options(self): - options = {} - if self.zyngui.state_manager.zynmixer.get_mono(self.chain.mixer_chan): - options['\u2612 Mono'] = 'mono' - else: - options['\u2610 Mono'] = 'mono' - if self.zyngui.state_manager.zynmixer.get_phase(self.chain.mixer_chan): - options['\u2612 Phase reverse'] = 'phase' - else: - options['\u2610 Phase reverse'] = 'phase' - if self.zyngui.state_manager.zynmixer.get_ms(self.chain.mixer_chan): - options['\u2612 M+S'] = 'ms' - else: - options['\u2610 M+S'] = 'ms' - - self.zyngui.screens['option'].config( - "Audio options", options, self.audio_menu_cb) - self.zyngui.show_screen('option') - - def audio_menu_cb(self, options, params): - if params == 'mono': - self.zyngui.state_manager.zynmixer.toggle_mono( - self.chain.mixer_chan) - elif params == 'ms': - self.zyngui.state_manager.zynmixer.toggle_ms(self.chain.mixer_chan) - elif params == 'phase': - self.zyngui.state_manager.zynmixer.toggle_phase( - self.chain.mixer_chan) - self.audio_options() - def chain_audio_capture(self): self.zyngui.screens['audio_in'].set_chain(self.chain) self.zyngui.show_screen('audio_in') @@ -360,15 +280,6 @@ def chain_midi_capture(self): self.zyngui.screens['midi_config'].input = True self.zyngui.show_screen('midi_config') - def toggle_recording(self): - if self.processor and self.processor.engine and self.processor.engine.name == 'AudioPlayer': - self.zyngui.state_manager.audio_recorder.toggle_recording( - self.processor) - else: - self.zyngui.state_manager.audio_recorder.toggle_recording( - self.zyngui.state_manager.audio_player) - self.fill_list() - def move_chain(self): self.zyngui.screens["audio_mixer"].moving_chain = True self.zyngui.show_screen_reset('audio_mixer') @@ -412,7 +323,7 @@ def remove_cb(self): options = {} if self.chain.synth_slots and self.chain.get_processor_count("MIDI Tool"): options['Remove All MIDI-FXs'] = "midifx" - if self.chain.get_processor_count("Audio Effect"): + if self.chain.get_processor_count("Audio Effect") > 1: options['Remove All Audio-FXs'] = "audiofx" if self.chain_id != 0: options['Remove Chain'] = "chain" @@ -440,11 +351,7 @@ def chain_remove_confirmed(self, params=None): def audiofx_add(self): self.zyngui.modify_chain( - {"type": "Audio Effect", "chain_id": self.chain_id, "post_fader": False}) - - def postfader_add(self): - self.zyngui.modify_chain( - {"type": "Audio Effect", "chain_id": self.chain_id, "post_fader": True}) + {"type": "Audio Effect", "chain_id": self.chain_id}) def remove_all_audiofx(self): self.zyngui.show_confirm( diff --git a/zyngui/zynthian_gui_dpm.py b/zyngui/zynthian_gui_dpm.py index 539b4b1f6..f36fe6763 100644 --- a/zyngui/zynthian_gui_dpm.py +++ b/zyngui/zynthian_gui_dpm.py @@ -6,7 +6,7 @@ # Zynthian GUI Digital Audio Peak Meters # # Copyright (C) 2015-2023 Fernando Moyano -# Copyright (C) 2015-2023 Brian Walton +# Copyright (C) 2015-2024 Brian Walton # # ****************************************************************************** # diff --git a/zyngui/zynthian_gui_engine.py b/zyngui/zynthian_gui_engine.py index 0ef68d7ed..e166db671 100644 --- a/zyngui/zynthian_gui_engine.py +++ b/zyngui/zynthian_gui_engine.py @@ -362,35 +362,9 @@ def select_action(self, i, t='S'): self.zyngui.modify_chain_status["engine"] = engine if "chain_id" in self.zyngui.modify_chain_status: # Modifying existing chain - if "processor" in self.zyngui.modify_chain_status: - # Replacing processor - pass - else: - slot_count = self.chain_manager.get_slot_count( - self.zyngui.modify_chain_status["chain_id"], self.zyngui.modify_chain_status["type"]) - if self.zyngui.modify_chain_status["type"] == "Audio Effect": - # Check for fader position - post_fader = "post_fader" in self.zyngui.modify_chain_status and self.zyngui.modify_chain_status[ - "post_fader"] - fader_pos = self.chain_manager.get_chain( - self.zyngui.modify_chain_status["chain_id"]).fader_pos - if post_fader and slot_count > fader_pos or not post_fader and slot_count > 0: - ask_parallel = True - else: - ask_parallel = False - else: - ask_parallel = slot_count > 0 - if ask_parallel: - # Adding to slot with existing processor - choose parallel/series - self.zyngui.screens['option'].config( - "Chain Mode", {"Series": False, "Parallel": True}, self.cb_add_parallel) - self.zyngui.show_screen('option') - return - else: - self.zyngui.modify_chain_status["parallel"] = False + pass else: # Adding engine to new chain - self.zyngui.modify_chain_status["parallel"] = False if engine == "AP": # TODO: Better done with engine flag self.zyngui.modify_chain_status["audio_thru"] = False @@ -427,10 +401,6 @@ def switch(self, swi, t='S'): self.show_details() return True - def cb_add_parallel(self, option, value): - self.zyngui.modify_chain_status['parallel'] = value - self.zyngui.modify_chain() - def set_selector(self, zs_hidden=False): super().set_selector(zs_hidden) self.zselector.zctrl.engine = self diff --git a/zyngui/zynthian_gui_main_menu.py b/zyngui/zynthian_gui_main_menu.py index b7a6e57f9..f94612c6e 100644 --- a/zyngui/zynthian_gui_main_menu.py +++ b/zyngui/zynthian_gui_main_menu.py @@ -28,6 +28,7 @@ # Zynthian specific modules from zyngui import zynthian_gui_config from zyngui.zynthian_gui_selector import zynthian_gui_selector +import zynautoconnect # ------------------------------------------------------------------------------ # Zynthian App Selection GUI Class @@ -43,23 +44,17 @@ def fill_list(self): self.list_data = [] # Chain & Sequence Management - try: - self.zyngui.chain_manager.get_next_free_mixer_chan() - mixer_avail = True - except: - mixer_avail = False self.list_data.append((None, 0, "> ADD CHAIN")) - if mixer_avail: - self.list_data.append( - (self.add_synth_chain, 0, "Add Instrument Chain")) - self.list_data.append((self.add_audiofx_chain, 0, "Add Audio Chain")) + self.list_data.append( + (self.add_synth_chain, 0, "Add Instrument Chain")) + self.list_data.append((self.add_audio_chain, 0, "Add Audio Chain")) + self.list_data.append((self.add_audiofx_chain, 0, "Add Audio FX Loop")) self.list_data.append((self.add_midifx_chain, 0, "Add MIDI Chain")) - if mixer_avail: - self.list_data.append( - (self.add_midiaudiofx_chain, 0, "Add MIDI+Audio Chain")) - self.list_data.append( - (self.add_generator_chain, 0, "Add Audio Generator Chain")) - self.list_data.append((self.add_special_chain, 0, "Add Special Chain")) + self.list_data.append( + (self.add_midiaudiofx_chain, 0, "Add MIDI+Audio Chain")) + self.list_data.append( + (self.add_generator_chain, 0, "Add Audio Generator Chain")) + self.list_data.append((self.add_special_chain, 0, "Add Special Chain")) self.list_data.append((None, 0, "> REMOVE")) self.list_data.append((self.remove_sequences, 0, "Remove Sequences")) @@ -93,10 +88,14 @@ def add_synth_chain(self, t='S'): self.zyngui.modify_chain( {"type": "MIDI Synth", "midi_thru": False, "audio_thru": False}) - def add_audiofx_chain(self, t='S'): + def add_audio_chain(self, t='S'): self.zyngui.modify_chain( {"type": "Audio Effect", "midi_thru": False, "audio_thru": True}) + def add_audiofx_chain(self, t='S'): + self.zyngui.modify_chain( + {"type": "Audio Effect", "midi_thru": True, "audio_thru": True, "fx_loop": True}) + def add_midifx_chain(self, t='S'): self.zyngui.modify_chain( {"type": "MIDI Tool", "midi_thru": True, "audio_thru": False}) diff --git a/zyngui/zynthian_gui_mixer.py b/zyngui/zynthian_gui_mixer.py index f4cc71e41..ed0e88ff7 100644 --- a/zyngui/zynthian_gui_mixer.py +++ b/zyngui/zynthian_gui_mixer.py @@ -68,6 +68,7 @@ def __init__(self, parent, x, y, width, height): self.hidden = False self.chain_id = None self.chain = None + self.mixer_proc = None self.hidden = True @@ -113,8 +114,6 @@ def __init__(self, parent, x, y, width, height): self.button_txcol = zynthian_gui_config.color_tx self.left_color = "#00AA00" self.right_color = "#00EE00" - self.learn_color_hl = "#999999" - self.learn_color = "#777777" self.high_color = "#CCCC00" # yellow self.rec_color = "#CC0000" # red @@ -128,8 +127,6 @@ def __init__(self, parent, x, y, width, height): self.font_fader = (zynthian_gui_config.font_family, int(0.9 * font_size)) self.font_icons = ("forkawesome", int(0.3 * self.width)) - self.font_learn = (zynthian_gui_config.font_family, - int(0.7 * font_size)) self.fader_text_limit = self.fader_top + int(0.1 * self.fader_height) @@ -157,9 +154,6 @@ def __init__(self, parent, x, y, width, height): self.dpm_b = zynthian_gui_dpm(self.zynmixer, None, 1, self.parent.main_canvas, self.dpm_b_x0, self.dpm_y0, self.dpm_width, self.fader_height, True, (f"strip:{self.fader_bg}", f"audio_strip:{self.fader_bg}")) - self.mono_text = self.parent.main_canvas.create_text(int(self.dpm_b_x0 + self.dpm_width / 2), int( - self.fader_top + self.fader_height / 2), text="??", state=tkinter.HIDDEN) - # Solo button self.solo = self.parent.main_canvas.create_rectangle(x, 0, x + self.width, self.button_height, fill=self.button_bgcol, width=0, tags=( f"solo_button:{self.fader_bg}", f"strip:{self.fader_bg}", f"audio_strip:{self.fader_bg}")) @@ -183,8 +177,6 @@ def __init__(self, parent, x, y, width, height): fill=self.left_color, width=0, tags=(f"strip:{self.fader_bg}", f"balance:{self.fader_bg}", f"audio_strip:{self.fader_bg}")) self.balance_right = self.parent.main_canvas.create_rectangle(self.fader_centre_x + 1, self.balance_top, self.width, self.balance_top + self.balance_height, fill=self.right_color, width=0, tags=(f"strip:{self.fader_bg}", f"balance:{self.fader_bg}", f"audio_strip:{self.fader_bg}")) - self.balance_text = self.parent.main_canvas.create_text(self.fader_centre_x, int( - self.balance_top + self.balance_height / 2) - 1, text="??", font=self.font_learn, state=tkinter.HIDDEN) self.parent.main_canvas.tag_bind( f"balance:{self.fader_bg}", "", self.on_balance_press) @@ -240,8 +232,9 @@ def hide(self): def show(self): """ Function to show mixer strip """ - self.dpm_a.set_strip(self.chain.mixer_chan) - self.dpm_b.set_strip(self.chain.mixer_chan) + if self.chain.is_audio(): + self.dpm_a.set_strip(self.mixer_proc.mixer_chan) + self.dpm_b.set_strip(self.mixer_proc.mixer_chan) self.parent.main_canvas.itemconfig(f"strip:{self.fader_bg}", state=tkinter.NORMAL) try: if not self.chain.is_audio(): @@ -251,26 +244,17 @@ def show(self): self.hidden = False self.draw_control() - def get_ctrl_learn_text(self, ctrl): - if not self.chain.is_audio(): - return "" - try: - param = self.zynmixer.get_learned_cc(self.zctrls[ctrl]) - return f"{param[0] + 1}#{param[1]}" - except: - return "??" - def draw_dpm(self, state): """ Function to draw the DPM level meter for a mixer strip state = [dpm_a, dpm_b, hold_a, hold_b, mono] """ - if self.hidden or self.chain.mixer_chan is None: + if self.hidden or self.mixer_proc.mixer_chan is None: return self.dpm_a.refresh(state[0], state[2], state[4]) self.dpm_b.refresh(state[1], state[3], state[4]) def draw_balance(self): - balance = self.zynmixer.get_balance(self.chain.mixer_chan) + balance = self.zctrls["balance"].value if balance is None: return if balance > 0: @@ -288,92 +272,45 @@ def draw_balance(self): self.x + self.width / 2, self.balance_top, self.x + self.width * balance / 2 + self.width, self.balance_top + self.balance_height) - if self.parent.zynmixer.midi_learn_zctrl == self.zctrls["balance"]: - lcolor = self.learn_color_hl - rcolor = self.learn_color - txcolor = zynthian_gui_config.color_ml - txstate = tkinter.NORMAL - text = "??" - elif self.parent.zynmixer.midi_learn_zctrl: - lcolor = self.learn_color_hl - rcolor = self.learn_color - txcolor = zynthian_gui_config.color_hl - txstate = tkinter.NORMAL - text = f"{self.get_ctrl_learn_text('balance')}" - else: - lcolor = self.left_color - rcolor = self.right_color - txcolor = self.button_txcol - txstate = tkinter.HIDDEN - text = "" - - self.parent.main_canvas.itemconfig(self.balance_left, fill=lcolor) - self.parent.main_canvas.itemconfig(self.balance_right, fill=rcolor) - self.parent.main_canvas.itemconfig( - self.balance_text, state=txstate, text=text, fill=txcolor) + self.parent.main_canvas.itemconfig(self.balance_left, fill=self.left_color) + self.parent.main_canvas.itemconfig(self.balance_right, fill=self.right_color) def draw_level(self): - level = self.zynmixer.get_level(self.chain.mixer_chan) + level = self.zctrls["level"].value if level is not None: self.parent.main_canvas.coords(self.fader, self.x, self.fader_top + self.fader_height * ( 1 - level), self.x + self.fader_width, self.fader_bottom) def draw_fader(self): - if self.zctrls and self.parent.zynmixer.midi_learn_zctrl == self.zctrls["level"]: - self.parent.main_canvas.coords( - self.fader_text, self.fader_centre_x, self.fader_centre_y - 2) - self.parent.main_canvas.itemconfig(self.fader_text, text="??", font=self.font_learn, angle=0, - fill=zynthian_gui_config.color_ml, justify=tkinter.CENTER, anchor=tkinter.CENTER) - elif self.parent.zynmixer.midi_learn_zctrl: - text = self.get_ctrl_learn_text('level') - self.parent.main_canvas.coords( - self.fader_text, self.fader_centre_x, self.fader_centre_y - 2) - self.parent.main_canvas.itemconfig(self.fader_text, text=text, font=self.font_learn, angle=0, - fill=zynthian_gui_config.color_hl, justify=tkinter.CENTER, anchor=tkinter.CENTER) + if self.chain is not None: + label_parts = self.chain.get_description( + 2).split("\n") + [""] # TODO else: - if self.chain is not None: - label_parts = self.chain.get_description( - 2).split("\n") + [""] # TODO - else: - label_parts = ["No info"] - - for i, label in enumerate(label_parts): - # self.parent.main_canvas.itemconfig(self.fader_text, text=label, state=tkinter.NORMAL) - bounds = self.parent.main_canvas.bbox(self.fader_text) - if bounds[1] < self.fader_text_limit: - while bounds and bounds[1] < self.fader_text_limit: - label = label[:-1] - self.parent.main_canvas.itemconfig( - self.fader_text, text=label) - bounds = self.parent.main_canvas.bbox(self.fader_text) - label_parts[i] = label + "..." - self.parent.main_canvas.itemconfig(self.fader_text, text="\n".join( - label_parts), font=self.font_fader, angle=90, fill=self.legend_txt_color, justify=tkinter.LEFT, anchor=tkinter.NW) - self.parent.main_canvas.coords( - self.fader_text, self.x, self.fader_bottom - 2) + label_parts = ["No info"] + + for i, label in enumerate(label_parts): + # self.parent.main_canvas.itemconfig(self.fader_text, text=label, state=tkinter.NORMAL) + bounds = self.parent.main_canvas.bbox(self.fader_text) + if bounds[1] < self.fader_text_limit: + while bounds and bounds[1] < self.fader_text_limit: + label = label[:-1] + self.parent.main_canvas.itemconfig( + self.fader_text, text=label) + bounds = self.parent.main_canvas.bbox(self.fader_text) + label_parts[i] = label + "..." + self.parent.main_canvas.itemconfig(self.fader_text, text="\n".join( + label_parts), font=self.font_fader, angle=90, fill=self.legend_txt_color, justify=tkinter.LEFT, anchor=tkinter.NW) + self.parent.main_canvas.coords( + self.fader_text, self.x, self.fader_bottom - 2) def draw_solo(self): txcolor = self.button_txcol font = self.font text = "S" - if self.zynmixer.get_solo(self.chain.mixer_chan): - if self.parent.zynmixer.midi_learn_zctrl: - bgcolor = self.learn_color_hl - else: - bgcolor = self.solo_color + if self.zctrls["solo"].value: + bgcolor = self.solo_color else: - if self.parent.zynmixer.midi_learn_zctrl: - bgcolor = self.learn_color - else: - bgcolor = self.button_bgcol - - if self.parent.zynmixer.midi_learn_zctrl == self.zctrls["solo"]: - txcolor = zynthian_gui_config.color_ml - elif self.parent.zynmixer.midi_learn_zctrl: - txcolor = zynthian_gui_config.color_hl - font = self.font_learn - text = f"S {self.get_ctrl_learn_text('solo')}" - + bgcolor = self.button_bgcol self.parent.main_canvas.itemconfig(self.solo, fill=bgcolor) self.parent.main_canvas.itemconfig( self.solo_text, text=text, font=font, fill=txcolor) @@ -381,42 +318,16 @@ def draw_solo(self): def draw_mute(self): txcolor = self.button_txcol font = self.font_icons - if self.zynmixer.get_mute(self.chain.mixer_chan): - if self.parent.zynmixer.midi_learn_zctrl: - bgcolor = self.learn_color_hl - else: - bgcolor = self.mute_color + if self.zctrls["mute"].value: + bgcolor = self.mute_color text = "\uf32f" else: - if self.parent.zynmixer.midi_learn_zctrl: - bgcolor = self.learn_color - else: - bgcolor = self.button_bgcol + bgcolor = self.button_bgcol text = "\uf028" - - if self.parent.zynmixer.midi_learn_zctrl == self.zctrls["mute"]: - txcolor = zynthian_gui_config.color_ml - elif self.parent.zynmixer.midi_learn_zctrl: - txcolor = zynthian_gui_config.color_hl - font = self.font_learn - text = f"\uf32f {self.get_ctrl_learn_text('mute')}" - self.parent.main_canvas.itemconfig(self.mute, fill=bgcolor) self.parent.main_canvas.itemconfig( self.mute_text, text=text, font=font, fill=txcolor) - def draw_mono(self): - """ - if self.zynmixer.get_mono(self.chain.mixer_chan): - self.parent.main_canvas.itemconfig(self.dpm_l_a, fill=self.mono_color) - self.parent.main_canvas.itemconfig(self.dpm_l_b, fill=self.mono_color) - self.dpm_hold_color = "#FFFFFF" - else: - self.parent.main_canvas.itemconfig(self.dpm_l_a, fill=self.low_color) - self.parent.main_canvas.itemconfig(self.dpm_l_b, fill=self.low_color) - self.dpm_hold_color = "#00FF00" - """ - def draw_control(self, control=None): """ Function to draw a mixer strip UI control control: Name of control or None to redraw all controls in the strip @@ -472,11 +383,8 @@ def draw_control(self, control=None): if control in [None, 'balance']: self.draw_balance() - if control in [None, 'mono']: - self.draw_mono() - - if control in [None, 'rec']: - if self.chain.is_audio() and self.parent.zyngui.state_manager.audio_recorder.is_armed(self.chain.mixer_chan): + if control in [None, 'record']: + if self.zctrls['record'].value: if self.parent.zyngui.state_manager.audio_recorder.status: self.parent.main_canvas.itemconfig( self.record_indicator, fill=self.rec_color, state=tkinter.NORMAL) @@ -541,17 +449,17 @@ def set_chain(self, chain_id): self.dpm_a.set_strip(None) self.dpm_b.set_strip(None) else: - if self.chain.mixer_chan is not None and self.chain.mixer_chan < len(self.parent.zynmixer.zctrls): - self.zctrls = self.parent.zynmixer.zctrls[self.chain.mixer_chan] + for slot in self.chain.audio_slots: + if slot[0].eng_code == "AM": + self.mixer_proc = slot[0] + self.zctrls = slot[0].controllers_dict self.show() def set_volume(self, value): """ Function to set volume value value: Volume value (0..1) """ - if self.parent.zynmixer.midi_learn_zctrl: - self.parent.enter_midi_learn(self.zctrls["level"]) - elif self.zctrls: + if self.zctrls: self.zctrls['level'].set_value(value) def get_volume(self): @@ -563,18 +471,14 @@ def get_volume(self): def nudge_volume(self, dval): """ Function to nudge volume """ - if self.parent.zynmixer.midi_learn_zctrl: - self.parent.enter_midi_learn(self.zctrls["level"]) - elif self.zctrls: + if self.zctrls: self.zctrls["level"].nudge(dval) def set_balance(self, value): """ Function to set balance value value: Balance value (-1..1) """ - if self.parent.zynmixer.midi_learn_zctrl: - self.parent.enter_midi_learn(self.zctrls["balance"]) - elif self.zctrls: + if self.zctrls: self.zctrls["balance"].set_value(value) def get_balance(self): """ Function to get balance value @@ -585,10 +489,7 @@ def get_balance(self): def nudge_balance(self, dval): """ Function to nudge balance """ - if self.parent.zynmixer.midi_learn_zctrl: - self.parent.enter_midi_learn(self.zctrls["balance"]) - self.parent.refresh_visible_strips() - elif self.zctrls: + if self.zctrls: self.zctrls['balance'].nudge(dval) def reset_volume(self): @@ -604,9 +505,7 @@ def set_mute(self, value): """ Function to set mute value: Mute value (True/False) """ - if self.parent.zynmixer.midi_learn_zctrl: - self.parent.enter_midi_learn(self.zctrls["mute"]) - elif self.zctrls: + if self.zctrls: self.zctrls['mute'].set_value(value) # self.parent.refresh_visible_strips() @@ -614,9 +513,7 @@ def set_solo(self, value): """ Function to set solo value: Solo value (True/False) """ - if self.parent.zynmixer.midi_learn_zctrl: - self.parent.enter_midi_learn(self.zctrls["solo"]) - elif self.zctrls: + if self.zctrls: self.zctrls['solo'].set_value(value) if self.chain_id == 0: self.parent.refresh_visible_strips() @@ -625,9 +522,7 @@ def set_mono(self, value): """ Function to toggle mono value: Mono value (True/False) """ - if self.parent.zynmixer.midi_learn_zctrl: - self.parent.enter_midi_learn(self.zctrls["mono"]) - elif self.zctrls: + if self.zctrls: self.zctrls['mono'].set_value(value) self.parent.refresh_visible_strips() @@ -718,9 +613,7 @@ def on_balance_press(self, event): """ Function to handle mouse click / touch of balance event: Mouse event """ - if self.parent.zynmixer.midi_learn_zctrl: - if self.parent.zynmixer.midi_learn_zctrl != self.zctrls["balance"]: - self.parent.zynmixer.midi_learn_zctrl = self.zctrls["balance"] + pass def on_balance_wheel_down(self, event): """ Function to handle mouse wheel down over balance @@ -753,8 +646,6 @@ def on_strip_release(self, event): if zynthian_gui_config.zyngui.cb_touch_release(event): return "break" - if self.parent.zynmixer.midi_learn_zctrl: - return if self.strip_drag_start and not self.dragging: delta = event.time - self.strip_drag_start.time if delta > 400: @@ -810,16 +701,12 @@ def __init__(self): super().__init__(has_backbutton=False) self.zynmixer = self.zyngui.state_manager.zynmixer - self.zynmixer.set_midi_learn_cb(self.enter_midi_learn) - self.MAIN_MIXBUS_STRIP_INDEX = self.zynmixer.MAX_NUM_CHANNELS - 1 - self.chan2strip = [None] * (self.MAIN_MIXBUS_STRIP_INDEX + 1) + self.chan2strip = [None] * (self.zynmixer.MAX_NUM_CHANNELS) self.highlighted_strip = None # highligted mixer strip object self.moving_chain = False # True if moving a chain left/right # List of (strip,control) requiring gui refresh (control=None for whole strip refresh) self.pending_refresh_queue = set() - # TODO: Should avoid duplicating midi_learn_zctrl from zynmixer but would need more safeguards to make change. - self.midi_learn_sticky = None # Maximum quantity of mixer strips to display (Defines strip width. Main always displayed.) visible_chains = zynthian_gui_config.visible_mixer_strips @@ -874,9 +761,9 @@ def __init__(self): self.main_mixbus_strip = zynthian_gui_mixer_strip( self, self.width - self.fader_width - 1, 0, self.fader_width - 1, self.height) self.main_mixbus_strip.set_chain(0) - self.main_mixbus_strip.zctrls = self.zynmixer.zctrls[self.MAIN_MIXBUS_STRIP_INDEX] + self.main_mixbus_strip.zctrls = self.zynmixer.processors[0].controllers_dict - self.zynmixer.enable_dpm(0, self.MAIN_MIXBUS_STRIP_INDEX, False) + self.zynmixer.enable_dpm(0, 0, False) self.refresh_visible_strips() @@ -906,32 +793,30 @@ def hide(self): if self.shown: if not self.zyngui.osc_clients: self.zynmixer.enable_dpm( - 0, self.MAIN_MIXBUS_STRIP_INDEX - 1, False) - if not self.midi_learn_sticky: - self.exit_midi_learn() - zynsigman.unregister( - zynsigman.S_AUDIO_MIXER, self.zynmixer.SS_ZCTRL_SET_VALUE, self.update_control) - zynsigman.unregister( - zynsigman.S_STATE_MAN, self.zyngui.state_manager.SS_LOAD_ZS3, self.cb_load_zs3) - zynsigman.unregister( - zynsigman.S_CHAIN_MAN, self.zyngui.chain_manager.SS_SET_ACTIVE_CHAIN, self.update_active_chain) - zynsigman.unregister( - zynsigman.S_AUDIO_RECORDER, zynthian_audio_recorder.SS_AUDIO_RECORDER_ARM, self.update_control_arm) - zynsigman.unregister( - zynsigman.S_AUDIO_RECORDER, zynthian_audio_recorder.SS_AUDIO_RECORDER_STATE, self.update_control_rec) - zynsigman.unregister( - zynsigman.S_AUDIO_PLAYER, zynthian_engine_audioplayer.SS_AUDIO_PLAYER_STATE, self.update_control_play) + 1, self.zynmixer.MAX_NUM_CHANNELS - 1, False) + zynsigman.unregister( + zynsigman.S_AUDIO_MIXER, self.zynmixer.SS_ZCTRL_SET_VALUE, self.update_control) + zynsigman.unregister( + zynsigman.S_STATE_MAN, self.zyngui.state_manager.SS_LOAD_ZS3, self.cb_load_zs3) + zynsigman.unregister( + zynsigman.S_CHAIN_MAN, self.zyngui.chain_manager.SS_SET_ACTIVE_CHAIN, self.update_active_chain) + zynsigman.unregister( + zynsigman.S_AUDIO_RECORDER, zynthian_audio_recorder.SS_AUDIO_RECORDER_ARM, self.update_control_arm) + zynsigman.unregister( + zynsigman.S_AUDIO_RECORDER, zynthian_audio_recorder.SS_AUDIO_RECORDER_STATE, self.update_control_rec) + zynsigman.unregister( + zynsigman.S_AUDIO_PLAYER, zynthian_engine_audioplayer.SS_AUDIO_PLAYER_STATE, self.update_control_play) super().hide() def build_view(self): """ Function to handle showing display """ - if zynthian_gui_config.enable_touch_navigation and self.moving_chain or self.zynmixer.midi_learn_zctrl: + if zynthian_gui_config.enable_touch_navigation and self.moving_chain : self.show_back_button() self.set_title() if zynthian_gui_config.enable_dpm: - self.zynmixer.enable_dpm(0, self.MAIN_MIXBUS_STRIP_INDEX, True) + self.zynmixer.enable_dpm(0, self.zynmixer.MAX_NUM_CHANNELS - 1, True) else: # Reset all DPM which will not be updated by refresh for strip in self.visible_mixer_strips: @@ -939,21 +824,18 @@ def build_view(self): self.highlight_active_chain(True) self.setup_zynpots() - if self.midi_learn_sticky: - self.enter_midi_learn(self.midi_learn_sticky) - else: - zynsigman.register( - zynsigman.S_AUDIO_MIXER, self.zynmixer.SS_ZCTRL_SET_VALUE, self.update_control) - zynsigman.register_queued( - zynsigman.S_STATE_MAN, self.zyngui.state_manager.SS_LOAD_ZS3, self.cb_load_zs3) - zynsigman.register_queued( - zynsigman.S_CHAIN_MAN, self.zyngui.chain_manager.SS_SET_ACTIVE_CHAIN, self.update_active_chain) - zynsigman.register_queued( - zynsigman.S_AUDIO_RECORDER, zynthian_audio_recorder.SS_AUDIO_RECORDER_ARM, self.update_control_arm) - zynsigman.register_queued( - zynsigman.S_AUDIO_RECORDER, zynthian_audio_recorder.SS_AUDIO_RECORDER_STATE, self.update_control_rec) - zynsigman.register_queued( - zynsigman.S_AUDIO_PLAYER, zynthian_engine_audioplayer.SS_AUDIO_PLAYER_STATE, self.update_control_play) + zynsigman.register( + zynsigman.S_AUDIO_MIXER, self.zynmixer.SS_ZCTRL_SET_VALUE, self.update_control) + zynsigman.register_queued( + zynsigman.S_STATE_MAN, self.zyngui.state_manager.SS_LOAD_ZS3, self.cb_load_zs3) + zynsigman.register_queued( + zynsigman.S_CHAIN_MAN, self.zyngui.chain_manager.SS_SET_ACTIVE_CHAIN, self.update_active_chain) + zynsigman.register_queued( + zynsigman.S_AUDIO_RECORDER, zynthian_audio_recorder.SS_AUDIO_RECORDER_ARM, self.update_control_arm) + zynsigman.register_queued( + zynsigman.S_AUDIO_RECORDER, zynthian_audio_recorder.SS_AUDIO_RECORDER_STATE, self.update_control_rec) + zynsigman.register_queued( + zynsigman.S_AUDIO_PLAYER, zynthian_engine_audioplayer.SS_AUDIO_PLAYER_STATE, self.update_control_play) return True def update_layout(self): @@ -968,15 +850,15 @@ def refresh_status(self): if self.shown: super().refresh_status() # Update main chain DPM - state = self.zynmixer.get_dpm_states(255, 255)[0] + state = self.zynmixer.get_dpm_states(0, 0)[0] self.main_mixbus_strip.draw_dpm(state) # Update other chains DPM if zynthian_gui_config.enable_dpm: states = self.zynmixer.get_dpm_states( - 0, self.MAIN_MIXBUS_STRIP_INDEX - 1) + 0, self.zynmixer.MAX_NUM_CHANNELS - 1) for strip in self.visible_mixer_strips: - if not strip.hidden and strip.chain.mixer_chan is not None: - state = states[strip.chain.mixer_chan] + if not strip.hidden and strip.chain.is_audio(): + state = states[strip.mixer_proc.mixer_chan] strip.draw_dpm(state) def plot_zctrls(self): @@ -991,7 +873,7 @@ def update_control(self, chan, symbol, value): """Mixer control update signal handler """ strip = self.chan2strip[chan] - if not strip or not strip.chain or strip.chain.mixer_chan is None: + if not strip or not strip.chain or strip.mixer_proc.mixer_chan is None: return self.pending_refresh_queue.add((strip, symbol)) if symbol == "level": @@ -1010,13 +892,13 @@ def update_control(self, chan, symbol, value): def update_control_arm(self, chan, value): """Function to handle audio recorder arm """ - self.update_control(chan, "rec", value) + self.update_control(chan, "record", value) def update_control_rec(self, state): """ Function to handle audio recorder status """ for strip in self.visible_mixer_strips: - self.pending_refresh_queue.add((strip, "rec")) + self.pending_refresh_queue.add((strip, "record")) def update_control_play(self, handle, state): """ Function to handle audio play status @@ -1090,8 +972,8 @@ def refresh_visible_strips(self): strip = self.visible_mixer_strips[strip_index] strip.set_chain(chain_id) # strip.draw_control() - if strip.chain.mixer_chan is not None and strip.chain.mixer_chan < len(self.chan2strip): - self.chan2strip[strip.chain.mixer_chan] = strip + if strip.chain.is_audio() and strip.mixer_proc.mixer_chan < len(self.chan2strip): + self.chan2strip[strip.mixer_proc.mixer_chan] = strip if chain_id == self.zyngui.chain_manager.active_chain_id: active_strip = strip strip_index += 1 @@ -1101,7 +983,7 @@ def refresh_visible_strips(self): strip.set_chain(None) strip.zctrls = None - self.chan2strip[self.MAIN_MIXBUS_STRIP_INDEX] = self.main_mixbus_strip + self.chan2strip[0] = self.main_mixbus_strip self.main_mixbus_strip.draw_control() return active_strip @@ -1118,10 +1000,7 @@ def switch_select(self, type='S'): if self.moving_chain: self.end_moving_chain() elif type == "S": - if self.zynmixer.midi_learn_zctrl: - self.midi_learn_menu() - else: - self.zyngui.chain_control() + self.zyngui.chain_control() elif type == "B": # Chain Options self.zyngui.screens['chain_options'].setup( @@ -1131,11 +1010,6 @@ def switch_select(self, type='S'): return False return True - # Handle onscreen back button press => Should we use it for entering MIDI learn? - # def backbutton_short_touch_action(self): - # if not self.back_action(): - # self.enter_midi_learn() - def back_action(self): """ Function to handle BACK action @@ -1144,9 +1018,6 @@ def back_action(self): if self.moving_chain: self.end_moving_chain() return True - elif self.zynmixer.midi_learn_zctrl: - self.exit_midi_learn() - return True else: return super().back_action() @@ -1162,13 +1033,8 @@ def switch(self, swi, t): if self.highlighted_strip is not None: self.highlighted_strip.toggle_solo() return True - elif t == "B" and self.zynmixer.midi_learn_zctrl: - self.midi_learn_menu() - return True elif swi == 1: - # if zynthian_gui_config.enable_touch_navigation and self.zynmixer.midi_learn_zctrl: - # return False # This is ugly, but it's the only way i figured for MIDI-learning "mute" without touch. # Moving the "learn" button to back is not an option. It's a labeled button on V4!! if t == "S" and not self.moving_chain: @@ -1176,11 +1042,8 @@ def switch(self, swi, t): self.highlighted_strip.toggle_mute() return True elif t == "B": - if self.zynmixer.midi_learn_zctrl: - self.back_action() - else: - self.zyngui.cuia_screen_zynpad() - return True + self.zyngui.cuia_screen_zynpad() + return True elif swi == 3: return self.switch_select(t) @@ -1281,102 +1144,7 @@ def on_wheel(self, event): self.mixer_strip_offset += 1 self.highlight_active_chain() - # -------------------------------------------------------------------------- - # MIDI learning management - # -------------------------------------------------------------------------- - def toggle_menu(self): - if self.zynmixer.midi_learn_zctrl: - self.midi_learn_menu() - else: - self.zyngui.toggle_screen("main_menu") - - def midi_learn_menu(self): - options = {} - try: - strip_id = self.zynmixer.midi_learn_zctrl.graph_path[0] + 1 - if strip_id == 17: - strip_id = "Main" - title = f"MIDI Learn Options ({strip_id})" - except: - title = f"MIDI Learn Options" - - if not self.zynmixer.midi_learn_zctrl: - options["Enable MIDI learn" ] = "enable" - - if isinstance(self.zynmixer.midi_learn_zctrl, zynthian_controller): - if self.zynmixer.midi_learn_zctrl.is_toggle: - if self.zynmixer.midi_learn_zctrl.midi_cc_momentary_switch: - options["\u2612 Momentary => Latch"] = "latched" - else: - options["\u2610 Momentary => Latch"] = "momentary" - if isinstance(self.zynmixer.midi_learn_zctrl, zynthian_controller): - options[f"Clean MIDI-learn ({self.zynmixer.midi_learn_zctrl.symbol})"] = "clean" - else: - options["Clean MIDI-learn (ALL)"] = "clean" - - self.midi_learn_sticky = self.zynmixer.midi_learn_zctrl - self.zyngui.screens['option'].config( - title, options, self.midi_learn_menu_cb) - self.zyngui.show_screen('option') - - def midi_learn_menu_cb(self, options, params): - if params == 'clean': - self.midi_unlearn_action() - elif params == 'enable': - self.enter_midi_learn() - self.zyngui.show_screen("audio_mixer") - elif params == "latched": - self.zynmixer.midi_learn_zctrl.midi_cc_momentary_switch = 0 - elif params == "momentary": - self.zynmixer.midi_learn_zctrl.midi_cc_momentary_switch = 1 - - def enter_midi_learn(self, zctrl=True): - self.midi_learn_sticky = None - if self.zynmixer.midi_learn_zctrl == zctrl: - return - self.zynmixer.midi_learn_zctrl = zctrl - if zctrl != True: - self.zynmixer.enable_midi_learn(zctrl) - self.refresh_visible_strips() - if zynthian_gui_config.enable_touch_navigation: - self.show_back_button(True) - - def exit_midi_learn(self): - if self.zynmixer.midi_learn_zctrl: - self.zynmixer.midi_learn_zctrl = None - self.zynmixer.disable_midi_learn() - self.refresh_visible_strips() - if zynthian_gui_config.enable_touch_navigation: - self.show_back_button(False) - - def toggle_midi_learn(self): - """ Pre-select all controls in a chain to allow selection of actual control to MIDI learn - """ - match self.zynmixer.midi_learn_zctrl: - case True: - self.exit_midi_learn() - case None: - self.enter_midi_learn(True) - case _: - self.enter_midi_learn() - - def midi_unlearn_action(self): - self.midi_learn_sticky = self.zynmixer.midi_learn_zctrl - if isinstance(self.zynmixer.midi_learn_zctrl, zynthian_controller): - self.zyngui.show_confirm( - f"Do you want to clear MIDI-learn for '{self.zynmixer.midi_learn_zctrl.name}' control?", - self.midi_unlearn_cb, self.zynmixer.midi_learn_zctrl) - else: - self.zyngui.show_confirm( - "Do you want to clean MIDI-learn for ALL mixer controls?", self.midi_unlearn_cb) - - def midi_unlearn_cb(self, zctrl=None): - if zctrl: - self.zynmixer.midi_unlearn(zctrl) - else: - self.zynmixer.midi_unlearn_all() - self.zynmixer.midi_learn_zctrl = True - self.refresh_visible_strips() + self.zyngui.toggle_screen("main_menu") # -------------------------------------------------------------------------- diff --git a/zyngui/zynthian_gui_processor_options.py b/zyngui/zynthian_gui_processor_options.py index 97b2b3437..4d6b4c729 100644 --- a/zyngui/zynthian_gui_processor_options.py +++ b/zyngui/zynthian_gui_processor_options.py @@ -58,24 +58,25 @@ def fill_list(self): self.list_data.append( (self.move_downchain, None, "Move down chain")) - if self.processor.type == "MIDI Synth": - eng_options = self.processor.engine.get_options() - if eng_options['replace']: + if self.processor.eng_code != "AM": + if self.processor.type == "MIDI Synth": + eng_options = self.processor.engine.get_options() + if eng_options['replace']: + self.list_data.append((self.replace, None, "Replace")) + else: self.list_data.append((self.replace, None, "Replace")) - else: - self.list_data.append((self.replace, None, "Replace")) - if self.processor.type == "MIDI Tool" or self.processor.type == "Audio Effect": - self.list_data.append((self.processor_remove, None, "Remove")) + if self.processor.type == "MIDI Tool" or self.processor.type == "Audio Effect": + self.list_data.append((self.processor_remove, None, "Remove")) - if len(self.processor.get_bank_list()) > 1 or len(self.processor.preset_list) > 0 and self.processor.preset_list[0][0] != '': - self.list_data.append((self.preset_list, None, "Preset List")) + if len(self.processor.get_bank_list()) > 1 or len(self.processor.preset_list) > 0 and self.processor.preset_list[0][0] != '': + self.list_data.append((self.preset_list, None, "Preset List")) - if hasattr(self.processor.engine, "save_preset"): - self.list_data.append((self.save_preset, None, "Save Preset")) + if hasattr(self.processor.engine, "save_preset"): + self.list_data.append((self.save_preset, None, "Save Preset")) - if self.processor.eng_code.startswith("JV/"): - self.list_data.append((self.scan_presets, None, "Scan for new presets")) + if self.processor.eng_code.startswith("JV/"): + self.list_data.append((self.scan_presets, None, "Scan for new presets")) self.list_data.append((self.midi_clean, None, "Clean MIDI-learn")) @@ -118,6 +119,8 @@ def processor_remove(self): def do_remove(self, unused=None): self.zyngui.chain_manager.remove_processor( self.chain_id, self.processor) + zynautoconnect.request_audio_connect(True) + zynautoconnect.request_midi_connect(True) self.chain = None self.chain_id = None self.processor = None @@ -138,7 +141,7 @@ def scan_presets(self): def midi_clean(self): if self.processor: self.zyngui.show_confirm( - f"Do you want to clean MIDI-learn for ALL controls in {self.processor.name} on MIDI channel {self.processor.midi_chan + 1}?", self.zyngui.chain_manager.clean_midi_learn, self.processor) + f"Do you want to clean MIDI-learn for ALL controls in {self.processor.name}?", self.zyngui.chain_manager.clean_midi_learn, self.processor) # FX-Chain management @@ -148,8 +151,6 @@ def can_move_upchain(self): return False if slot == 0: slots = self.chain.get_slots_by_type(self.processor.type) - if self.processor.type == "Audio Effect" and slot >= self.chain.fader_pos: - return True return len(slots[0]) > 1 return (slot is not None and slot > 0) @@ -164,8 +165,6 @@ def can_move_downchain(self): return False slots = self.chain.get_slots_by_type(self.processor.type) if slot >= len(slots) - 1: - if self.processor.type == "Audio Effect" and slot < self.chain.fader_pos: - return True return len(slots[0]) > 1 return (slot is not None and slot + 1 < self.chain.get_slot_count(self.processor.type)) diff --git a/zynlibs/zynaudioplayer/player.cpp b/zynlibs/zynaudioplayer/player.cpp index 8ff95e44f..04588bd9d 100644 --- a/zynlibs/zynaudioplayer/player.cpp +++ b/zynlibs/zynaudioplayer/player.cpp @@ -1503,6 +1503,8 @@ static void lib_exit(void) { remove_player(g_vPlayers.front()); } fprintf(stderr, "done!\n"); + jack_deactivate(g_jack_client); + jack_client_close(g_jack_client); } AUDIO_PLAYER* add_player() { diff --git a/zynlibs/zynmixer/mixer.c b/zynlibs/zynmixer/mixer.c index 0fee76c03..fb801b3b8 100644 --- a/zynlibs/zynmixer/mixer.c +++ b/zynlibs/zynmixer/mixer.c @@ -35,20 +35,25 @@ #include "tinyosc.h" #include // provides inet_pton +// #define DEBUG + +#define MAX_CHANNELS 32 +#define MAX_OSC_CLIENTS 5 + +static uint8_t CHANNEL = 0; +static uint8_t GROUP = 1; + +pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; char g_oscbuffer[1024]; // Used to send OSC messages -char g_oscpath[20]; //!@todo Ensure path length is sufficient for all paths, e.g. /mixer/faderxxx +char g_oscpath[64]; //!@todo Ensure path length is sufficient for all paths, e.g. /mixer/channels/xx/fader int g_oscfd = -1; // File descriptor for OSC socket int g_bOsc = 0; // True if OSC client subscribed pthread_t g_eventThread; // ID of low priority event thread int g_sendEvents = 1; // Set to 0 to exit event thread int g_solo = 0; // True if any channel solo enabled -// #define DEBUG - -#define MAX_CHANNELS 17 -#define MAX_OSC_CLIENTS 5 - -struct dynamic { +// Structure describing a channel strip +struct channel_strip { jack_port_t* inPortA; // Jack input port A jack_port_t* inPortB; // Jack input port B jack_port_t* outPortA; // Jack output port A @@ -57,31 +62,46 @@ struct dynamic { float reqlevel; // Requested fader level 0..1 float balance; // Current balance -1..+1 float reqbalance; // Requested balance -1..+1 + float send[MAX_CHANNELS]; // Current fx send levels float dpmA; // Current peak programme A-leg float dpmB; // Current peak programme B-leg float holdA; // Current peak hold level A-leg float holdB; // Current peak hold level B-leg + float dpmAlast; // Last peak programme A-leg + float dpmBlast; // Last peak programme B-leg + float holdAlast; // Last peak hold level A-leg + float holdBlast; // Last peak hold level B-leg uint8_t mute; // 1 if muted uint8_t solo; // 1 if solo uint8_t mono; // 1 if mono uint8_t ms; // 1 if MS decoding uint8_t phase; // 1 if channel B phase reversed uint8_t normalise; // 1 if channel normalised to main output (when output not routed) + uint8_t group; // 1 if strip is a group uint8_t inRouted; // 1 if source routed to channel uint8_t outRouted; // 1 if output routed uint8_t enable_dpm; // 1 to enable calculation of peak meter }; -jack_client_t* g_pJackClient; -struct dynamic g_dynamic[MAX_CHANNELS]; -struct dynamic g_dynamic_last[MAX_CHANNELS]; // Previous values used to thin OSC updates +struct fx_send { + jack_port_t* outPortA; // Jack output port A + jack_port_t* outPortB; // Jack output port B + float level; // Current fader level 0..1 + jack_default_audio_sample_t* bufferA; // Holds audio samples + jack_default_audio_sample_t* bufferB; // Holds audio samples +}; + +jack_client_t* g_chanJackClient; +jack_client_t* g_grpJackClient; +struct channel_strip* g_channelStrips[MAX_CHANNELS]; +struct fx_send* g_fxSends[MAX_CHANNELS]; unsigned int g_nDampingCount = 0; unsigned int g_nDampingPeriod = 10; // Quantity of cycles between applying DPM damping decay unsigned int g_nHoldCount = 0; float g_fDpmDecay = 0.9; // Factor to scale for DPM decay - defines resolution of DPM decay struct sockaddr_in g_oscClient[MAX_OSC_CLIENTS]; // Array of registered OSC clients char g_oscdpm[20]; -jack_nframes_t g_samplerate = 44100; // Jack samplerate used to calculate damping factor +jack_nframes_t g_samplerate = 48000; // Jack samplerate used to calculate damping factor jack_nframes_t g_buffersize = 1024; // Jack buffer size used to calculate damping factor jack_default_audio_sample_t* pNormalisedBufferA = NULL; // Pointer to buffer for normalised audio jack_default_audio_sample_t* pNormalisedBufferB = NULL; // Pointer to buffer for normalised audio @@ -121,76 +141,90 @@ void sendOscInt(const char* path, int value) { void* eventThreadFn(void* param) { while (g_sendEvents) { if (g_bOsc) { - for (unsigned int chan = 0; chan < MAX_CHANNELS; chan++) { - if ((int)(100000 * g_dynamic_last[chan].dpmA) != (int)(100000 * g_dynamic[chan].dpmA)) { - sprintf(g_oscdpm, "/mixer/dpm%da", chan); - sendOscFloat(g_oscdpm, convertToDBFS(g_dynamic[chan].dpmA)); - g_dynamic_last[chan].dpmA = g_dynamic[chan].dpmA; + for (unsigned int chan = 0; chan < MAX_CHANNELS; ++chan) { + if (g_channelStrips[chan] == NULL) + continue; + if ((int)(100000 * g_channelStrips[chan]->dpmAlast) != (int)(100000 * g_channelStrips[chan]->dpmA)) { + sprintf(g_oscdpm, "/mixer/channels/%d/dpma", chan); + sendOscFloat(g_oscdpm, convertToDBFS(g_channelStrips[chan]->dpmA)); + g_channelStrips[chan]->dpmAlast = g_channelStrips[chan]->dpmA; } - if ((int)(100000 * g_dynamic_last[chan].dpmB) != (int)(100000 * g_dynamic[chan].dpmB)) { - sprintf(g_oscdpm, "/mixer/dpm%db", chan); - sendOscFloat(g_oscdpm, convertToDBFS(g_dynamic[chan].dpmB)); - g_dynamic_last[chan].dpmB = g_dynamic[chan].dpmB; + if ((int)(100000 * g_channelStrips[chan]->dpmBlast) != (int)(100000 * g_channelStrips[chan]->dpmB)) { + sprintf(g_oscdpm, "/mixer/channels/%d/dpmb", chan); + sendOscFloat(g_oscdpm, convertToDBFS(g_channelStrips[chan]->dpmB)); + g_channelStrips[chan]->dpmBlast = g_channelStrips[chan]->dpmB; } - if ((int)(100000 * g_dynamic_last[chan].holdA) != (int)(100000 * g_dynamic[chan].holdA)) { - sprintf(g_oscdpm, "/mixer/hold%da", chan); - sendOscFloat(g_oscdpm, convertToDBFS(g_dynamic[chan].holdA)); - g_dynamic_last[chan].holdA = g_dynamic[chan].holdA; + if ((int)(100000 * g_channelStrips[chan]->holdAlast) != (int)(100000 * g_channelStrips[chan]->holdA)) { + sprintf(g_oscdpm, "/mixer/channels/%d/holda", chan); + sendOscFloat(g_oscdpm, convertToDBFS(g_channelStrips[chan]->holdA)); + g_channelStrips[chan]->holdAlast = g_channelStrips[chan]->holdA; } - if ((int)(100000 * g_dynamic_last[chan].holdB) != (int)(100000 * g_dynamic[chan].holdB)) { - sprintf(g_oscdpm, "/mixer/hold%db", chan); - sendOscFloat(g_oscdpm, convertToDBFS(g_dynamic[chan].holdB)); - g_dynamic_last[chan].holdB = g_dynamic[chan].holdB; + if ((int)(100000 * g_channelStrips[chan]->holdBlast) != (int)(100000 * g_channelStrips[chan]->holdB)) { + sprintf(g_oscdpm, "/mixer/channels/%d/holdb", chan); + sendOscFloat(g_oscdpm, convertToDBFS(g_channelStrips[chan]->holdB)); + g_channelStrips[chan]->holdBlast = g_channelStrips[chan]->holdB; } } } usleep(10000); } - pthread_exit(NULL); } -static int onJackProcess(jack_nframes_t nFrames, void* pArgs) { +static int onJackProcess(jack_nframes_t nFrames, void* args) { jack_default_audio_sample_t *pInA, *pInB, *pOutA, *pOutB, *pChanOutA, *pChanOutB; - unsigned int frame, chan; - float curLevelA, curLevelB, reqLevelA, reqLevelB, fDeltaA, fDeltaB, fSampleA, fSampleB, fSampleM; + float curLevelA, curLevelB, reqLevelA, reqLevelB, fDeltaA, fDeltaB, fSampleA, fSampleB, fSampleM, fSendA[MAX_CHANNELS], fSendB[MAX_CHANNELS]; + uint8_t grp = *((uint8_t*)args); + + pthread_mutex_lock(&mutex); // Clear the normalisation buffer. This will be populated by each channel then used in final channel iteration memset(pNormalisedBufferA, 0.0, nFrames * sizeof(jack_default_audio_sample_t)); memset(pNormalisedBufferB, 0.0, nFrames * sizeof(jack_default_audio_sample_t)); + // Clear send buffers. + for (uint8_t send = 0; send < MAX_CHANNELS; ++send) { + if (g_fxSends[send]) { + memset(g_fxSends[send]->bufferA, 0.0, nFrames * sizeof(jack_default_audio_sample_t)); + memset(g_fxSends[send]->bufferB, 0.0, nFrames * sizeof(jack_default_audio_sample_t)); + } + } + // Process each channel for (chan = 0; chan < MAX_CHANNELS; chan++) { - if (isChannelRouted(chan) || (chan == (MAX_CHANNELS - 1))) { - //**Calculate processing levels** + if (g_channelStrips[chan] == NULL) + continue; + + if (g_channelStrips[chan]->inRouted || chan == 0) { + // Calculate processing levels // Calculate current (last set) balance - if (g_dynamic[chan].balance > 0.0) - curLevelA = g_dynamic[chan].level * (1 - g_dynamic[chan].balance); + if (g_channelStrips[chan]->balance > 0.0) + curLevelA = g_channelStrips[chan]->level * (1 - g_channelStrips[chan]->balance); else - curLevelA = g_dynamic[chan].level; - if (g_dynamic[chan].balance < 0.0) - curLevelB = g_dynamic[chan].level * (1 + g_dynamic[chan].balance); + curLevelA = g_channelStrips[chan]->level; + if (g_channelStrips[chan]->balance < 0.0) + curLevelB = g_channelStrips[chan]->level * (1 + g_channelStrips[chan]->balance); else - curLevelB = g_dynamic[chan].level; + curLevelB = g_channelStrips[chan]->level; // Calculate mute and target level and balance (that we will fade to over this cycle period to avoid abrupt change clicks) - if (g_dynamic[chan].mute || g_solo && (chan < MAX_CHANNELS - 1) && g_dynamic[chan].solo != 1) { + if (g_channelStrips[chan]->mute || g_solo && (chan) && g_channelStrips[chan]->solo != 1) { // Do not mute aux if solo enabled - g_dynamic[chan].level = 0; // We can set this here because we have the data and will iterate towards 0 over this frame + g_channelStrips[chan]->level = 0; // We can set this here because we have the data and will iterate towards 0 over this frame reqLevelA = 0.0; reqLevelB = 0.0; } else { - if (g_dynamic[chan].reqbalance > 0.0) - reqLevelA = g_dynamic[chan].reqlevel * (1 - g_dynamic[chan].reqbalance); + if (g_channelStrips[chan]->reqbalance > 0.0) + reqLevelA = g_channelStrips[chan]->reqlevel * (1 - g_channelStrips[chan]->reqbalance); else - reqLevelA = g_dynamic[chan].reqlevel; - if (g_dynamic[chan].reqbalance < 0.0) - reqLevelB = g_dynamic[chan].reqlevel * (1 + g_dynamic[chan].reqbalance); + reqLevelA = g_channelStrips[chan]->reqlevel; + if (g_channelStrips[chan]->reqbalance < 0.0) + reqLevelB = g_channelStrips[chan]->reqlevel * (1 + g_channelStrips[chan]->reqbalance); else - reqLevelB = g_dynamic[chan].reqlevel; - g_dynamic[chan].level = g_dynamic[chan].reqlevel; - g_dynamic[chan].balance = g_dynamic[chan].reqbalance; + reqLevelB = g_channelStrips[chan]->reqlevel; + g_channelStrips[chan]->level = g_channelStrips[chan]->reqlevel; + g_channelStrips[chan]->balance = g_channelStrips[chan]->reqbalance; } // Calculate the step change for each leg to apply on each sample in buffer for fade between last and this period's level @@ -199,13 +233,13 @@ static int onJackProcess(jack_nframes_t nFrames, void* pArgs) { // **Apply processing to audio samples** - pInA = jack_port_get_buffer(g_dynamic[chan].inPortA, nFrames); - pInB = jack_port_get_buffer(g_dynamic[chan].inPortB, nFrames); + pInA = jack_port_get_buffer(g_channelStrips[chan]->inPortA, nFrames); + pInB = jack_port_get_buffer(g_channelStrips[chan]->inPortB, nFrames); - if (isChannelOutRouted(chan)) { + if (g_channelStrips[chan]->outRouted) { // Direct output so create audio buffers - pChanOutA = jack_port_get_buffer(g_dynamic[chan].outPortA, nFrames); - pChanOutB = jack_port_get_buffer(g_dynamic[chan].outPortB, nFrames); + pChanOutA = jack_port_get_buffer(g_channelStrips[chan]->outPortA, nFrames); + pChanOutB = jack_port_get_buffer(g_channelStrips[chan]->outPortB, nFrames); memset(pChanOutA, 0.0, nFrames * sizeof(jack_default_audio_sample_t)); memset(pChanOutB, 0.0, nFrames * sizeof(jack_default_audio_sample_t)); } else { @@ -214,7 +248,7 @@ static int onJackProcess(jack_nframes_t nFrames, void* pArgs) { // Iterate samples, scaling each and adding to output and set DPM if any samples louder than current DPM for (frame = 0; frame < nFrames; frame++) { - if (chan == MAX_CHANNELS - 1) { + if (chan == 0) { // Mix channel input and normalised channels mix fSampleA = (pInA[frame] + pNormalisedBufferA[frame]); fSampleB = (pInB[frame] + pNormalisedBufferB[frame]); @@ -223,18 +257,18 @@ static int onJackProcess(jack_nframes_t nFrames, void* pArgs) { fSampleB = pInB[frame]; } // Handle channel phase reverse - if (g_dynamic[chan].phase) + if (g_channelStrips[chan]->phase) fSampleB = -fSampleB; // Decode M+S - if (g_dynamic[chan].ms) { + if (g_channelStrips[chan]->ms) { fSampleM = fSampleA + fSampleB; fSampleB = fSampleA - fSampleB; fSampleA = fSampleM; } // Handle mono - if (g_dynamic[chan].mono) { + if (g_channelStrips[chan]->mono) { fSampleA = (fSampleA + fSampleB) / 2.0; fSampleB = fSampleA; } @@ -254,8 +288,19 @@ static int onJackProcess(jack_nframes_t nFrames, void* pArgs) { pChanOutA[frame] += fSampleA; pChanOutB[frame] += fSampleB; } + for (uint8_t send = 0; send < MAX_CHANNELS; ++send) { + if (g_fxSends[send]) { + g_fxSends[send]->bufferA[frame] += fSampleA * g_channelStrips[chan]->send[send]; + g_fxSends[send]->bufferB[frame] += fSampleB * g_channelStrips[chan]->send[send]; + if(isinf(g_fxSends[send]->bufferA[frame])) + g_fxSends[send]->bufferA[frame] = 1.0; + if(isinf(g_fxSends[send]->bufferB[frame])) + g_fxSends[send]->bufferB[frame] = 1.0; + } + } + // Write normalised samples - if (chan < MAX_CHANNELS - 1 && g_dynamic[chan].normalise) { + if (g_channelStrips[chan]->normalise) { pNormalisedBufferA[frame] += fSampleA; pNormalisedBufferB[frame] += fSampleB; } @@ -264,36 +309,36 @@ static int onJackProcess(jack_nframes_t nFrames, void* pArgs) { curLevelB += fDeltaB; // Process DPM - if (g_dynamic[chan].enable_dpm) { + if (g_channelStrips[chan]->enable_dpm) { fSampleA = fabs(fSampleA); - if (fSampleA > g_dynamic[chan].dpmA) - g_dynamic[chan].dpmA = fSampleA; + if (fSampleA > g_channelStrips[chan]->dpmA) + g_channelStrips[chan]->dpmA = fSampleA; fSampleB = fabs(fSampleB); - if (fSampleB > g_dynamic[chan].dpmB) - g_dynamic[chan].dpmB = fSampleB; + if (fSampleB > g_channelStrips[chan]->dpmB) + g_channelStrips[chan]->dpmB = fSampleB; // Update peak hold and scale DPM for damped release - if (g_dynamic[chan].dpmA > g_dynamic[chan].holdA) - g_dynamic[chan].holdA = g_dynamic[chan].dpmA; - if (g_dynamic[chan].dpmB > g_dynamic[chan].holdB) - g_dynamic[chan].holdB = g_dynamic[chan].dpmB; + if (g_channelStrips[chan]->dpmA > g_channelStrips[chan]->holdA) + g_channelStrips[chan]->holdA = g_channelStrips[chan]->dpmA; + if (g_channelStrips[chan]->dpmB > g_channelStrips[chan]->holdB) + g_channelStrips[chan]->holdB = g_channelStrips[chan]->dpmB; } } if (g_nHoldCount == 0) { // Only update peak hold each g_nHoldCount cycles - g_dynamic[chan].holdA = g_dynamic[chan].dpmA; - g_dynamic[chan].holdB = g_dynamic[chan].dpmB; + g_channelStrips[chan]->holdA = g_channelStrips[chan]->dpmA; + g_channelStrips[chan]->holdB = g_channelStrips[chan]->dpmB; } if (g_nDampingCount == 0) { // Only update damping release each g_nDampingCount cycles - g_dynamic[chan].dpmA *= g_fDpmDecay; - g_dynamic[chan].dpmB *= g_fDpmDecay; + g_channelStrips[chan]->dpmA *= g_fDpmDecay; + g_channelStrips[chan]->dpmB *= g_fDpmDecay; } - } else if (g_dynamic[chan].enable_dpm) { - g_dynamic[chan].dpmA = -200.0; - g_dynamic[chan].dpmB = -200.0; - g_dynamic[chan].holdA = -200.0; - g_dynamic[chan].holdB = -200.0; + } else if (g_channelStrips[chan]->enable_dpm) { + g_channelStrips[chan]->dpmA = -200.0; + g_channelStrips[chan]->dpmB = -200.0; + g_channelStrips[chan]->holdA = -200.0; + g_channelStrips[chan]->holdB = -200.0; } } @@ -306,21 +351,25 @@ static int onJackProcess(jack_nframes_t nFrames, void* pArgs) { else --g_nHoldCount; + pthread_mutex_unlock(&mutex); return 0; } void onJackConnect(jack_port_id_t source, jack_port_id_t dest, int connect, void* args) { - uint8_t chan; - for (chan = 0; chan < MAX_CHANNELS; chan++) { - if (jack_port_connected(g_dynamic[chan].inPortA) > 0 || (jack_port_connected(g_dynamic[chan].inPortB) > 0)) - g_dynamic[chan].inRouted = 1; + pthread_mutex_lock(&mutex); + for (uint8_t chan = 0; chan < MAX_CHANNELS; chan++) { + if (g_channelStrips[chan] == NULL) + continue; + if (jack_port_connected(g_channelStrips[chan]->inPortA) > 0 || (jack_port_connected(g_channelStrips[chan]->inPortB) > 0)) + g_channelStrips[chan]->inRouted = 1; else - g_dynamic[chan].inRouted = 0; - if (jack_port_connected(g_dynamic[chan].outPortA) > 0 || (jack_port_connected(g_dynamic[chan].outPortB) > 0)) - g_dynamic[chan].outRouted = 1; + g_channelStrips[chan]->inRouted = 0; + if (jack_port_connected(g_channelStrips[chan]->outPortA) > 0 || (jack_port_connected(g_channelStrips[chan]->outPortB) > 0)) + g_channelStrips[chan]->outRouted = 1; else - g_dynamic[chan].outRouted = 0; + g_channelStrips[chan]->outRouted = 0; } + pthread_mutex_unlock(&mutex); } int onJackSamplerate(jack_nframes_t nSamplerate, void* arg) { @@ -336,14 +385,29 @@ int onJackBuffersize(jack_nframes_t nBuffersize, void* arg) { return 0; g_buffersize = nBuffersize; g_nDampingPeriod = g_fDpmDecay * g_samplerate / g_buffersize / 15; + pthread_mutex_lock(&mutex); free(pNormalisedBufferA); free(pNormalisedBufferB); - pNormalisedBufferA = malloc(nBuffersize * sizeof(jack_default_audio_sample_t)); - pNormalisedBufferB = malloc(nBuffersize * sizeof(jack_default_audio_sample_t)); + pNormalisedBufferA = malloc(g_buffersize * sizeof(jack_default_audio_sample_t)); + pNormalisedBufferB = malloc(g_buffersize * sizeof(jack_default_audio_sample_t)); + for (uint8_t chan = 0; chan < MAX_CHANNELS; ++chan) { + if (g_fxSends[chan]) { + g_fxSends[chan]->bufferA = jack_port_get_buffer(g_fxSends[chan]->outPortA, g_buffersize); + g_fxSends[chan]->bufferB = jack_port_get_buffer(g_fxSends[chan]->outPortB, g_buffersize); + } + } + pthread_mutex_unlock(&mutex); return 0; } int init() { + fprintf(stderr, "zynmixer starting\n"); + + for (uint8_t chan = 0; chan < MAX_CHANNELS; ++chan) { + g_channelStrips[chan] = NULL; + g_fxSends[chan] = NULL; + } + // Initialsize OSC g_oscfd = socket(AF_INET, SOCK_DGRAM, 0); for (uint8_t i = 0; i < MAX_OSC_CLIENTS; ++i) { @@ -358,69 +422,57 @@ int init() { jack_status_t nStatus; jack_options_t nOptions = JackNoStartServer; - if ((g_pJackClient = jack_client_open("zynmixer", nOptions, &nStatus, sServerName)) == 0) { - fprintf(stderr, "libzynmixer: Failed to start jack client: %d\n", nStatus); + if ((g_chanJackClient = jack_client_open("zynmixer_chans", nOptions, &nStatus, sServerName)) == 0) { + fprintf(stderr, "libzynmixer: Failed to start channel jack client: %d\n", nStatus); exit(1); } #ifdef DEBUG fprintf(stderr, "libzynmixer: Registering as '%s'.\n", jack_get_client_name(g_pJackClient)); #endif - // Create input ports - for (size_t chan = 0; chan < MAX_CHANNELS; ++chan) { - g_dynamic[chan].level = 0.0; - g_dynamic[chan].reqlevel = 0.8; - g_dynamic[chan].balance = 0.0; - g_dynamic[chan].reqbalance = 0.0; - g_dynamic[chan].mute = 0; - g_dynamic[chan].ms = 0; - g_dynamic[chan].phase = 0; - g_dynamic[chan].enable_dpm = 1; - g_dynamic[chan].normalise = 1; - char sName[11]; - sprintf(sName, "input_%02lda", chan + 1); - if (!(g_dynamic[chan].inPortA = jack_port_register(g_pJackClient, sName, JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0))) { - fprintf(stderr, "libzynmixer: Cannot register %s\n", sName); - exit(1); - } - sprintf(sName, "input_%02ldb", chan + 1); - if (!(g_dynamic[chan].inPortB = jack_port_register(g_pJackClient, sName, JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0))) { - fprintf(stderr, "libzynmixer: Cannot register %s\n", sName); - exit(1); - } - sprintf(sName, "output_%02lda", chan + 1); - if (!(g_dynamic[chan].outPortA = jack_port_register(g_pJackClient, sName, JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0))) { - fprintf(stderr, "libzynmixer: Cannot register %s\n", sName); - exit(1); - } - sprintf(sName, "output_%02ldb", chan + 1); - if (!(g_dynamic[chan].outPortB = jack_port_register(g_pJackClient, sName, JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0))) { - fprintf(stderr, "libzynmixer: Cannot register %s\n", sName); - exit(1); - } - g_dynamic_last[chan].dpmA = 100.0; - g_dynamic_last[chan].dpmB = 100.0; - g_dynamic_last[chan].holdA = 100.0; - g_dynamic_last[chan].holdB = 100.0; + if ((g_grpJackClient = jack_client_open("zynmixer_buses", nOptions, &nStatus, sServerName)) == 0) { + fprintf(stderr, "libzynmixer: Failed to start group jack client: %d\n", nStatus); + exit(1); } +#ifdef DEBUG + fprintf(stderr, "libzynmixer: Registering as '%s'.\n", jack_get_client_name(g_pJackClient)); +#endif + + // Create main mixbus channel strip + addStrip(1); + + // Temporarily create a port on chanstips client to work around bug in jack + jack_port_t* tmp_port = jack_port_register(g_chanJackClient, "tmp", JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0); #ifdef DEBUG - fprintf(stderr, "libzynmixer: Created input ports\n"); + fprintf(stderr, "libzynmixer: Created channel strips\n"); #endif // Register the cleanup function to be called when library exits atexit(end); // Register the callbacks - jack_set_process_callback(g_pJackClient, onJackProcess, 0); - jack_set_port_connect_callback(g_pJackClient, onJackConnect, 0); - jack_set_sample_rate_callback(g_pJackClient, onJackSamplerate, 0); - jack_set_buffer_size_callback(g_pJackClient, onJackBuffersize, 0); + jack_set_process_callback(g_chanJackClient, onJackProcess, &CHANNEL); + jack_set_port_connect_callback(g_chanJackClient, onJackConnect, &CHANNEL); + jack_set_sample_rate_callback(g_chanJackClient, onJackSamplerate, &CHANNEL); + jack_set_buffer_size_callback(g_chanJackClient, onJackBuffersize, &CHANNEL); + + jack_set_process_callback(g_grpJackClient, onJackProcess, &GROUP); + jack_set_port_connect_callback(g_grpJackClient, onJackConnect, &GROUP); + jack_set_sample_rate_callback(g_grpJackClient, onJackSamplerate, &GROUP); + jack_set_buffer_size_callback(g_grpJackClient, onJackBuffersize, &GROUP); - if (jack_activate(g_pJackClient)) { + if (jack_activate(g_chanJackClient)) { fprintf(stderr, "libzynmixer: Cannot activate client\n"); exit(1); } + if (jack_activate(g_grpJackClient)) { + fprintf(stderr, "libzynmixer: Cannot activate client\n"); + exit(1); + } + + // Remove tmp port + jack_port_unregister(g_chanJackClient, tmp_port); #ifdef DEBUG fprintf(stderr, "libzynmixer: Activated client\n"); @@ -441,129 +493,157 @@ int init() { } void end() { - if (g_pJackClient) { - // Mute output and wait for soft mute to occur before closing link with jack server - setLevel(MAX_CHANNELS - 1, 0.0); - usleep(100000); - // jack_client_close(g_pJackClient); - } g_sendEvents = 0; - free(pNormalisedBufferA); - free(pNormalisedBufferB); - void* status; pthread_join(g_eventThread, &status); + + //Soft mute output + setLevel(0, 0.0); + usleep(100000); + + // Close links with jack server + if (g_grpJackClient) { + jack_deactivate(g_grpJackClient); + jack_client_close(g_grpJackClient); + } + if (g_chanJackClient) { + jack_deactivate(g_chanJackClient); + jack_client_close(g_chanJackClient); + } + + // Release dynamically created resources + free(pNormalisedBufferA); + free(pNormalisedBufferB); + for (uint8_t chan = 0; chan < MAX_CHANNELS; ++chan) { + free(g_channelStrips[chan]); + free(g_fxSends[chan]); + } + fprintf(stderr, "zynmixer ended\n"); } void setLevel(uint8_t channel, float level) { - if (channel >= MAX_CHANNELS) - channel = MAX_CHANNELS - 1; - else - g_dynamic[channel].reqlevel = level; - sprintf(g_oscpath, "/mixer/fader%d", channel); + if (channel >= MAX_CHANNELS || g_channelStrips[channel] == NULL) + return; + g_channelStrips[channel]->reqlevel = level; + sprintf(g_oscpath, "/mixer/channels/%d/fader", channel); sendOscFloat(g_oscpath, level); } float getLevel(uint8_t channel) { - if (channel >= MAX_CHANNELS) - channel = MAX_CHANNELS - 1; - return g_dynamic[channel].reqlevel; + if (channel >= MAX_CHANNELS || g_channelStrips[channel] == NULL) + return 0.0f; + return g_channelStrips[channel]->reqlevel; } void setBalance(uint8_t channel, float balance) { + if (channel >= MAX_CHANNELS || g_channelStrips[channel] == NULL) + return; if (fabs(balance) > 1) return; - if (channel >= MAX_CHANNELS) - channel = MAX_CHANNELS - 1; - g_dynamic[channel].reqbalance = balance; - sprintf(g_oscpath, "/mixer/balance%d", channel); + g_channelStrips[channel]->reqbalance = balance; + sprintf(g_oscpath, "/mixer/channels/%d/balance", channel); sendOscFloat(g_oscpath, balance); } float getBalance(uint8_t channel) { - if (channel >= MAX_CHANNELS) - channel = MAX_CHANNELS - 1; - return g_dynamic[channel].reqbalance; + if (channel >= MAX_CHANNELS || g_channelStrips[channel] == NULL) + return 0.0f; + return g_channelStrips[channel]->reqbalance; } void setMute(uint8_t channel, uint8_t mute) { - if (channel >= MAX_CHANNELS) - channel = MAX_CHANNELS - 1; - g_dynamic[channel].mute = mute; - sprintf(g_oscpath, "/mixer/mute%d", channel); + if (channel >= MAX_CHANNELS || g_channelStrips[channel] == NULL) + return; + g_channelStrips[channel]->mute = mute; + sprintf(g_oscpath, "/mixer/channels/%d/mute", channel); sendOscInt(g_oscpath, mute); } uint8_t getMute(uint8_t channel) { - if (channel >= MAX_CHANNELS) - channel = MAX_CHANNELS - 1; - return g_dynamic[channel].mute; + if (channel >= MAX_CHANNELS || g_channelStrips[channel] == NULL) + return 0; + return g_channelStrips[channel]->mute; } void setPhase(uint8_t channel, uint8_t phase) { - if (channel >= MAX_CHANNELS) - channel = MAX_CHANNELS - 1; - g_dynamic[channel].phase = phase; - sprintf(g_oscpath, "/mixer/phase%d", channel); + if (channel >= MAX_CHANNELS || g_channelStrips[channel] == NULL) + return; + g_channelStrips[channel]->phase = phase; + sprintf(g_oscpath, "/mixer/channels/%d/phase", channel); sendOscInt(g_oscpath, phase); } uint8_t getPhase(uint8_t channel) { - if (channel >= MAX_CHANNELS) + if (channel >= MAX_CHANNELS || g_channelStrips[channel] == NULL) return 0; - if (channel >= MAX_CHANNELS) - channel = MAX_CHANNELS - 1; - return g_dynamic[channel].phase; + return g_channelStrips[channel]->phase; +} + +void setSend(uint8_t channel, uint8_t send, float level) { + if (channel >= MAX_CHANNELS || g_channelStrips[channel] == NULL || send >= MAX_CHANNELS) + return; + g_channelStrips[channel]->send[send] = level; + sprintf(g_oscpath, "/mixer/channels/%d/send_%d", channel, send); + sendOscFloat(g_oscpath, level); +} + +float getSend(uint8_t channel, uint8_t send) { + if (channel >= MAX_CHANNELS || g_channelStrips[channel] == NULL || send >= MAX_CHANNELS) + return 0.0f; + return g_channelStrips[channel]->send[send]; } void setNormalise(uint8_t channel, uint8_t enable) { - if (channel >= MAX_CHANNELS) - channel = MAX_CHANNELS - 1; - g_dynamic[channel].normalise = enable; - sprintf(g_oscpath, "/mixer/normalise%d", channel); + if (channel == 0 || channel >= MAX_CHANNELS || g_channelStrips[channel] == NULL) + return; + g_channelStrips[channel]->normalise = enable; + sprintf(g_oscpath, "/mixer/channels/%d/normalise", channel); sendOscInt(g_oscpath, enable); } uint8_t getNormalise(uint8_t channel, uint8_t enable) { - if (channel >= MAX_CHANNELS) + if (channel >= MAX_CHANNELS || g_channelStrips[channel] == NULL) return 0; - if (channel >= MAX_CHANNELS) - channel = MAX_CHANNELS - 1; - return g_dynamic[channel].normalise; + return g_channelStrips[channel]->normalise; } void setSolo(uint8_t channel, uint8_t solo) { - if (channel + 1 >= MAX_CHANNELS) { + if (channel >= MAX_CHANNELS || g_channelStrips[channel] == NULL) + return; + if (channel == 0) { // Setting main mixbus solo will disable all channel solos - for (uint8_t nChannel = 0; nChannel < MAX_CHANNELS - 1; ++nChannel) { - g_dynamic[nChannel].solo = 0; - sprintf(g_oscpath, "/mixer/solo%d", nChannel); - sendOscInt(g_oscpath, 0); + for (uint8_t chan = 1; chan < MAX_CHANNELS; ++chan) { + if (g_channelStrips[chan]) { + g_channelStrips[chan]->solo = 0; + sprintf(g_oscpath, "/mixer/channels/%d/solo", chan); + sendOscInt(g_oscpath, 0); + } } } else { - g_dynamic[channel].solo = solo; - sprintf(g_oscpath, "/mixer/solo%d", channel); + g_channelStrips[channel]->solo = solo; + sprintf(g_oscpath, "/mixer/channels/%d/solo", channel); sendOscInt(g_oscpath, solo); } // Set the global solo flag if any channel solo is enabled g_solo = 0; - for (uint8_t nChannel = 0; nChannel < MAX_CHANNELS - 1; ++nChannel) - g_solo |= g_dynamic[nChannel].solo; - sprintf(g_oscpath, "/mixer/solo%d", MAX_CHANNELS - 1); + for (uint8_t chan = 1; chan < MAX_CHANNELS; ++chan) + if (g_channelStrips[chan]) + g_solo |= g_channelStrips[chan]->solo; + sprintf(g_oscpath, "/mixer/channels/%d/solo", 0); sendOscInt(g_oscpath, g_solo); } uint8_t getSolo(uint8_t channel) { - if (channel >= MAX_CHANNELS) - channel = MAX_CHANNELS - 1; - return g_dynamic[channel].solo; + if (channel >= MAX_CHANNELS || g_channelStrips[channel] == NULL) + return 0; + return g_channelStrips[channel]->solo; } void toggleMute(uint8_t channel) { + if (channel >= MAX_CHANNELS || g_channelStrips[channel] == NULL) + return; uint8_t mute; - if (channel >= MAX_CHANNELS) - channel = MAX_CHANNELS - 1; - mute = g_dynamic[channel].mute; + mute = g_channelStrips[channel]->mute; if (mute) setMute(channel, 0); else @@ -571,10 +651,10 @@ void toggleMute(uint8_t channel) { } void togglePhase(uint8_t channel) { + if (channel >= MAX_CHANNELS || g_channelStrips[channel] == NULL) + return; uint8_t phase; - if (channel >= MAX_CHANNELS) - channel = MAX_CHANNELS - 1; - phase = g_dynamic[channel].phase; + phase = g_channelStrips[channel]->phase; if (phase) setPhase(channel, 0); else @@ -582,68 +662,58 @@ void togglePhase(uint8_t channel) { } void setMono(uint8_t channel, uint8_t mono) { - if (channel >= MAX_CHANNELS) - channel = MAX_CHANNELS - 1; - g_dynamic[channel].mono = (mono != 0); - sprintf(g_oscpath, "/mixer/mono%d", channel); + if (channel >= MAX_CHANNELS || g_channelStrips[channel] == NULL) + return; + g_channelStrips[channel]->mono = (mono != 0); + sprintf(g_oscpath, "/mixer/channels/%d/mono", channel); sendOscInt(g_oscpath, mono); } uint8_t getMono(uint8_t channel) { - if (channel >= MAX_CHANNELS) - channel = MAX_CHANNELS - 1; - return g_dynamic[channel].mono; + if (channel >= MAX_CHANNELS || g_channelStrips[channel] == NULL) + return 0; + return g_channelStrips[channel]->mono; } void setMS(uint8_t channel, uint8_t enable) { - if (channel >= MAX_CHANNELS) - channel = MAX_CHANNELS - 1; - g_dynamic[channel].ms = enable != 0; + if (channel >= MAX_CHANNELS || g_channelStrips[channel] == NULL) + return; + g_channelStrips[channel]->ms = enable != 0; } uint8_t getMS(uint8_t channel) { - if (channel >= MAX_CHANNELS) - channel = MAX_CHANNELS - 1; - return g_dynamic[channel].ms; + if (channel >= MAX_CHANNELS || g_channelStrips[channel] == NULL) + return 0; + return g_channelStrips[channel]->ms; } void reset(uint8_t channel) { - if (channel >= MAX_CHANNELS) - channel = MAX_CHANNELS - 1; + if (channel >= MAX_CHANNELS || g_channelStrips[channel] == NULL) + return; setLevel(channel, 0.8); setBalance(channel, 0.0); setMute(channel, 0); setMono(channel, 0); setPhase(channel, 0); setSolo(channel, 0); -} - -uint8_t isChannelRouted(uint8_t channel) { - if (channel >= MAX_CHANNELS) - return 0; - return g_dynamic[channel].inRouted; -} - -uint8_t isChannelOutRouted(uint8_t channel) { - if (channel >= MAX_CHANNELS) - return 0; - return g_dynamic[channel].outRouted; + for (uint8_t send = 0; send < MAX_CHANNELS; ++send) + setSend(channel, send, 0); } float getDpm(uint8_t channel, uint8_t leg) { - if (channel >= MAX_CHANNELS) - channel = MAX_CHANNELS - 1; + if (channel >= MAX_CHANNELS || g_channelStrips[channel] == NULL) + return 0.0f; if (leg) - return convertToDBFS(g_dynamic[channel].dpmB); - return convertToDBFS(g_dynamic[channel].dpmA); + return convertToDBFS(g_channelStrips[channel]->dpmB); + return convertToDBFS(g_channelStrips[channel]->dpmA); } float getDpmHold(uint8_t channel, uint8_t leg) { - if (channel >= MAX_CHANNELS) - channel = MAX_CHANNELS - 1; + if (channel >= MAX_CHANNELS || g_channelStrips[channel] == NULL) + return 0.0f; if (leg) - return convertToDBFS(g_dynamic[channel].holdB); - return convertToDBFS(g_dynamic[channel].holdA); + return convertToDBFS(g_channelStrips[channel]->holdB); + return convertToDBFS(g_channelStrips[channel]->holdA); } void getDpmStates(uint8_t start, uint8_t end, float* values) { @@ -652,10 +722,6 @@ void getDpmStates(uint8_t start, uint8_t end, float* values) { start = end; end = tmp; } - if (end > MAX_CHANNELS) - end = MAX_CHANNELS; - if (start > MAX_CHANNELS) - start = MAX_CHANNELS; uint8_t count = end - start + 1; while (count--) { *(values++) = getDpm(start, 0); @@ -668,7 +734,7 @@ void getDpmStates(uint8_t start, uint8_t end, float* values) { } void enableDpm(uint8_t start, uint8_t end, uint8_t enable) { - struct dynamic* pChannel; + struct channel_strip* pChannel; if (start > end) { uint8_t tmp = start; start = end; @@ -678,14 +744,15 @@ void enableDpm(uint8_t start, uint8_t end, uint8_t enable) { start = MAX_CHANNELS - 1; if (end >= MAX_CHANNELS) end = MAX_CHANNELS - 1; - for (uint8_t channel = start; channel <= end; ++channel) { - pChannel = &(g_dynamic[channel]); - pChannel->enable_dpm = enable; - if (enable == 0) { - pChannel->dpmA = 0; - pChannel->dpmB = 0; - pChannel->holdA = 0; - pChannel->holdB = 0; + for (uint8_t chan = start; chan <= end; ++chan) { + if (g_channelStrips[chan] == NULL) + continue; + g_channelStrips[chan]->enable_dpm = enable; + if (g_channelStrips[chan] == 0) { + g_channelStrips[chan]->dpmA = 0; + g_channelStrips[chan]->dpmB = 0; + g_channelStrips[chan]->holdA = 0; + g_channelStrips[chan]->holdB = 0; } } } @@ -700,17 +767,19 @@ int addOscClient(const char* client) { return -1; } fprintf(stderr, "libzynmixer: Added OSC client %d: %s\n", i, client); - for (int nChannel = 0; nChannel < MAX_CHANNELS; ++nChannel) { - setBalance(nChannel, getBalance(nChannel)); - setLevel(nChannel, getLevel(nChannel)); - setMono(nChannel, getMono(nChannel)); - setMute(nChannel, getMute(nChannel)); - setPhase(nChannel, getPhase(nChannel)); - setSolo(nChannel, getSolo(nChannel)); - g_dynamic_last[nChannel].dpmA = 100.0; - g_dynamic_last[nChannel].dpmB = 100.0; - g_dynamic_last[nChannel].holdA = 100.0; - g_dynamic_last[nChannel].holdB = 100.0; + for (int chan = 0; chan < MAX_CHANNELS; ++chan) { + if (g_channelStrips[chan] == NULL) + continue; + setBalance(chan, getBalance(chan)); + setLevel(chan, getLevel(chan)); + setMono(chan, getMono(chan)); + setMute(chan, getMute(chan)); + setPhase(chan, getPhase(chan)); + setSolo(chan, getSolo(chan)); + g_channelStrips[chan]->dpmAlast = 100.0; + g_channelStrips[chan]->dpmBlast = 100.0; + g_channelStrips[chan]->holdAlast = 100.0; + g_channelStrips[chan]->holdBlast = 100.0; } g_bOsc = 1; return i; @@ -734,4 +803,112 @@ void removeOscClient(const char* client) { } } +int8_t addStrip(uint8_t grp) { + uint8_t chan; + for (chan = 0; chan < MAX_CHANNELS; ++chan) { + if (g_channelStrips[chan]) + continue; + g_channelStrips[chan] = malloc(sizeof(struct channel_strip)); + g_channelStrips[chan]->level = 0.0; + g_channelStrips[chan]->reqlevel = 0.8; + g_channelStrips[chan]->balance = 0.0; + g_channelStrips[chan]->reqbalance = 0.0; + g_channelStrips[chan]->mute = 0; + g_channelStrips[chan]->ms = 0; + g_channelStrips[chan]->phase = 0; + for (uint8_t send = 0; send < MAX_CHANNELS; ++send) + g_channelStrips[chan]->send[send] = 0.0; + g_channelStrips[chan]->enable_dpm = 1; + g_channelStrips[chan]->group = grp; + g_channelStrips[chan]->normalise = 0; //!@todo fix normaisation (chan != 0); + jack_client_t* client = g_chanJackClient; + if (grp) + client = g_grpJackClient; + char name[11]; + sprintf(name, "input_%02da", chan); + pthread_mutex_lock(&mutex); + if (!(g_channelStrips[chan]->inPortA = jack_port_register(client, name, JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0))) { + fprintf(stderr, "libzynmixer: Cannot register %s\n", name); + free(g_channelStrips[chan]); + pthread_mutex_unlock(&mutex); + return -1; + } + sprintf(name, "input_%02db", chan); + if (!(g_channelStrips[chan]->inPortB = jack_port_register(client, name, JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0))) { + fprintf(stderr, "libzynmixer: Cannot register %s\n", name); + jack_port_unregister(client, g_channelStrips[chan]->inPortA); + free(g_channelStrips[chan]); + g_channelStrips[chan] = NULL; + pthread_mutex_unlock(&mutex); + return -1; + } + sprintf(name, "output_%02da", chan); + if (!(g_channelStrips[chan]->outPortA = jack_port_register(client, name, JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0))) { + fprintf(stderr, "libzynmixer: Cannot register %s\n", name); + jack_port_unregister(client, g_channelStrips[chan]->inPortA); + jack_port_unregister(client, g_channelStrips[chan]->inPortB); + free(g_channelStrips[chan]); + g_channelStrips[chan] = NULL; + pthread_mutex_unlock(&mutex); + return -1; + } + sprintf(name, "output_%02db", chan); + if (!(g_channelStrips[chan]->outPortB = jack_port_register(client, name, JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0))) { + fprintf(stderr, "libzynmixer: Cannot register %s\n", name); + jack_port_unregister(client, g_channelStrips[chan]->inPortA); + jack_port_unregister(client, g_channelStrips[chan]->inPortB); + jack_port_unregister(client, g_channelStrips[chan]->outPortA); + free(g_channelStrips[chan]); + g_channelStrips[chan] = NULL; + pthread_mutex_unlock(&mutex); + return -1; + } + if (chan && grp) { + g_fxSends[chan] = malloc(sizeof(struct fx_send)); + sprintf(name, "send_%02da", chan); + g_fxSends[chan]->outPortA = jack_port_register(g_chanJackClient, name, JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0); + sprintf(name, "send_%02db", chan); + g_fxSends[chan]->outPortB = jack_port_register(g_chanJackClient, name, JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0); + g_fxSends[chan]->bufferA = jack_port_get_buffer(g_fxSends[chan]->outPortA, g_buffersize); + g_fxSends[chan]->bufferB = jack_port_get_buffer(g_fxSends[chan]->outPortB, g_buffersize); + g_fxSends[chan]->level = 1.0; + } + g_channelStrips[chan]->dpmAlast = 100.0; + g_channelStrips[chan]->dpmBlast = 100.0; + g_channelStrips[chan]->holdAlast = 100.0; + g_channelStrips[chan]->holdBlast = 100.0; + pthread_mutex_unlock(&mutex); + return chan; + } + return -1; +} + +int8_t removeStrip(uint8_t chan) { + if (chan == 0 || chan >= MAX_CHANNELS || g_channelStrips[chan] == NULL) + return -1; + + jack_client_t* client = g_chanJackClient; + if (g_fxSends[chan]){ + jack_port_unregister(g_chanJackClient, g_fxSends[chan]->outPortA); + jack_port_unregister(g_chanJackClient, g_fxSends[chan]->outPortB); + free(g_fxSends[chan]); + g_fxSends[chan] = NULL; + client = g_grpJackClient; + } + jack_port_unregister(client, g_channelStrips[chan]->inPortA); + jack_port_unregister(client, g_channelStrips[chan]->inPortB); + jack_port_unregister(client, g_channelStrips[chan]->outPortA); + jack_port_unregister(client, g_channelStrips[chan]->outPortB); + free(g_channelStrips[chan]); + g_channelStrips[chan] = NULL; + + return chan; +} + +int8_t isGroup(uint8_t chan) { + if (chan > MAX_CHANNELS || g_channelStrips[chan] == NULL) + return -1; + return g_channelStrips[chan]->group; +} + uint8_t getMaxChannels() { return MAX_CHANNELS; } diff --git a/zynlibs/zynmixer/mixer.h b/zynlibs/zynmixer/mixer.h index a8f0cad31..3e5b028a9 100644 --- a/zynlibs/zynmixer/mixer.h +++ b/zynlibs/zynmixer/mixer.h @@ -33,7 +33,8 @@ /** @brief Initialises library * @retval int 1 on success, 0 on fail */ -int init(); +int init() __attribute__((constructor)); + /** @brief Destroy library */ @@ -42,96 +43,106 @@ void end(); /** @brief Set channel level * @param channel Index of channel * @param level Channel level (0..1) - * @note Channel > MAX_CHANNELS will set master fader level */ void setLevel(uint8_t channel, float level); /** @brief Get channel level * @param channel Index of channel * @retval float Channel level (0..1) - * @note Channel > MAX_CHANNELS will retrived master fader level */ float getLevel(uint8_t channel); /** @brief Set channel balance * @param channel Index of channel * @param pan Channel pan (-1..1) - * @note Channel > MAX_CHANNELS will set master balance */ void setBalance(uint8_t channel, float pan); /** @brief Get channel balance * @param channel Index of channel * @retval float Channel pan (-1..1) - * @note Channel > MAX_CHANNELS will retrived master balance */ float getBalance(uint8_t channel); -/** @brief Set mute state of channel +/** @brief Set channel mute state * @param channel Index of channel * @param mute Mute status (0: Unmute, 1: Mute) */ void setMute(uint8_t channel, uint8_t mute); -/** @brief Get mute state of channel +/** @brief Get channel mute state * @param channel Index of channel * @retval uint8_t Mute status (0: Unmute, 1: Mute) */ uint8_t getMute(uint8_t channel); -/** @brief Set solo state of channel +/** @brief Set channel solo state * @param channel Index of channel * @param solo Solostatus (0: Normal, 1: Solo) */ void setSolo(uint8_t channel, uint8_t solo); -/** @brief Get solo state of channel +/** @brief Get channel solo state * @param channel Index of channel * @retval uint8_t Solo status (0: Normal, 1: solo) */ uint8_t getSolo(uint8_t channel); -/** @brief Toggles mute of a channel +/** @brief Toggles channel mute * @param channel Index of channel */ void toggleMute(uint8_t channel); -/** @brief Set mono state of channel +/** @brief Set channel mono state * @param channel Index of channel * @param mono (0: Stereo, 1: Mono) */ void setMono(uint8_t channel, uint8_t mono); -/** @brief Get mono state of channel +/** @brief Get channel mono state * @param channel Index of channel * @retval uint8_t Channel mono state (0: Stereo, 1: mono) */ uint8_t getMono(uint8_t channel); -/** @brief Enable MS decode mode +/** @brief Set channel MS decode mode * @param channel Index of channel * @param enable (0: Stereo, 1: MS decode) */ void setMS(uint8_t channel, uint8_t enable); -/** @brief Get MS decode mode +/** @brief Get channel MS decode mode * @param channel Index of channel * @retval uint8_t MS decode mode (0: Stereo, 1: MS decode) */ uint8_t getMS(uint8_t channel); -/** @brief Set phase state of channel +/** @brief Set channel phase state * @param channel Index of channel * @param phase (0: in phase, 1: phase reversed) */ void setPhase(uint8_t channel, uint8_t phase); -/** @brief Get phase state of channel +/** @brief Get channel phase state * @param channel Index of channel * @retval uint8_t Channel phase state (0: in phase, 1: phase reversed) */ uint8_t getPhase(uint8_t channel); +/** @brief Set channel fx send level + * @param channel Index of channel + * @param send Index of fx send + * @param level Channel level (0..1) + */ +void setSend(uint8_t channel, uint8_t send, float level); + +/** @brief Get channel fx send level + * @param channel Index of channel + * @param send Index of fx send + * @retval float Channel send level + */ +float getSend(uint8_t channel, uint8_t send); + /** @brief Set internal normalisation of channel * @param channel Index of channel * @param enable 1 to enable internal normalisation when channel direct output not routed @@ -149,18 +160,6 @@ uint8_t getNormalise(uint8_t channel, uint8_t enable); */ void reset(uint8_t channel); -/** @brief Check if channel has source routed - * @param channel Index of channel - * @retval uint8_t 1 if channel has source routed. 0 if no source routed to channel. - */ -uint8_t isChannelRouted(uint8_t channel); - -/** @brief Check if channel has output routed - * @param channel Index of channel - * @retval uint8_t 1 if channel has output routed. 0 if not routed. - */ -uint8_t isChannelOutRouted(uint8_t channel); - /** @brief Get DPM level * @param channel Index of channel * @param leg 0 for A leg (left), 1 for B leg (right) @@ -202,6 +201,24 @@ int addOscClient(const char* client); */ void removeOscClient(const char* client); +/** Add a channel strip + * @param uint8_t grp 1 to create a group otherwise create normal channel strip + * @retval int8_t Index of channel strip or -1 on failure + */ +int8_t addStrip(uint8_t grp); + +/** @brief Remove a channel strip + * @param chan Index of channel strip to remove + * @retval int8_t Index of port removed or -1 on failure + */ +int8_t removeStrip(uint8_t chan); + +/** @brief Check if strip is a group + * @param chan Index of channel strip to check + * @retval int8_t 1 if group, 0 if normal channel, -1 on failure +*/ +int8_t isGroup(uint8_t chan); + /** @brief Get maximum quantity of channels * @retval size_t Maximum quantity of channels */ diff --git a/zynlibs/zynseq/zynseq.cpp b/zynlibs/zynseq/zynseq.cpp index 4206dcdcd..ecc758202 100644 --- a/zynlibs/zynseq/zynseq.cpp +++ b/zynlibs/zynseq/zynseq.cpp @@ -57,7 +57,7 @@ static struct ev_start startEvents[128]; jack_port_t* g_pInputPort; // Pointer to the JACK input port jack_port_t* g_pOutputPort; // Pointer to the JACK output port jack_port_t* g_pMetronomePort; // Pointer to the JACK metronome audio output port -jack_client_t* g_pJackClient = NULL; // Pointer to the JACK client +jack_client_t* g_jackClient = NULL; // Pointer to the JACK client jack_nframes_t g_nSampleRate = 44100; // Quantity of samples per second uint32_t g_nXruns = 0; @@ -281,7 +281,7 @@ void onJackTimebase(jack_transport_state_t nState, jack_nframes_t nFramesInPerio updateBBT(pPosition); DPRINTF("Set position from frame %u\n", pPosition->frame); } - g_nTransportStartFrame = jack_frame_time(g_pJackClient) - pPosition->frame; //!@todo This isn't setting to transport start position + g_nTransportStartFrame = jack_frame_time(g_jackClient) - pPosition->frame; //!@todo This isn't setting to transport start position pPosition->valid = JackPositionBBT; g_dFramesPerClock = getFramesPerClock(g_dTempo); g_bTimebaseChanged = false; @@ -321,7 +321,7 @@ void onJackTimebase(jack_transport_state_t nState, jack_nframes_t nFramesInPerio For each event, add MIDI events to the output buffer at appropriate sample sequence Remove events from schedule */ -int onJackProcess(jack_nframes_t nFrames, void* pArgs) { +int onJackProcessChan(jack_nframes_t nFrames, void* pArgs) { static jack_position_t transportPosition; // JACK transport position structure populated each cycle and checked for transport progress static uint8_t nClock = PPQN; // Clock pulse count 0..PPQN - 1 static uint32_t nTicksPerPulse; @@ -336,8 +336,8 @@ int onJackProcess(jack_nframes_t nFrames, void* pArgs) { void* pOutputBuffer = jack_port_get_buffer(g_pOutputPort, nFrames); unsigned char* pBuffer; jack_midi_clear_buffer(pOutputBuffer); - jack_nframes_t nNow = jack_last_frame_time(g_pJackClient); - jack_transport_state_t nState = jack_transport_query(g_pJackClient, &transportPosition); + jack_nframes_t nNow = jack_last_frame_time(g_jackClient); + jack_transport_state_t nState = jack_transport_query(g_jackClient, &transportPosition); jack_default_audio_sample_t* pOutMetronome = (jack_default_audio_sample_t*)jack_port_get_buffer(g_pMetronomePort, nFrames); memset(pOutMetronome, 0, sizeof(jack_default_audio_sample_t) * nFrames); @@ -639,6 +639,9 @@ void end() { for (auto it : g_mSchedule) { delete it.second; } + jack_deactivate(g_jackClient); + jack_client_close(g_jackClient); + } // ** Library management functions ** @@ -659,43 +662,43 @@ void init(char* name) { jack_status_t nStatus; jack_options_t nOptions = JackNoStartServer; - if (g_pJackClient) { + if (g_jackClient) { fprintf(stderr, "libzynseq already initialised\n"); return; // Already initialised } - if ((g_pJackClient = jack_client_open(name, nOptions, &nStatus, sServerName)) == 0) { + if ((g_jackClient = jack_client_open(name, nOptions, &nStatus, sServerName)) == 0) { fprintf(stderr, "libzynseq failed to start jack client: %d\n", nStatus); return; } // Create input port - if (!(g_pInputPort = jack_port_register(g_pJackClient, "input", JACK_DEFAULT_MIDI_TYPE, JackPortIsInput, 0))) { + if (!(g_pInputPort = jack_port_register(g_jackClient, "input", JACK_DEFAULT_MIDI_TYPE, JackPortIsInput, 0))) { fprintf(stderr, "libzynseq cannot register input port\n"); return; } // Create output port - if (!(g_pOutputPort = jack_port_register(g_pJackClient, "output", JACK_DEFAULT_MIDI_TYPE, JackPortIsOutput, 0))) { + if (!(g_pOutputPort = jack_port_register(g_jackClient, "output", JACK_DEFAULT_MIDI_TYPE, JackPortIsOutput, 0))) { fprintf(stderr, "libzynseq cannot register output port\n"); return; } // Create metronome output port - if (!(g_pMetronomePort = jack_port_register(g_pJackClient, "metronome", JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0))) { + if (!(g_pMetronomePort = jack_port_register(g_jackClient, "metronome", JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0))) { fprintf(stderr, "linzynseq cannot register metronome port\n"); return; } - g_nSampleRate = jack_get_sample_rate(g_pJackClient); + g_nSampleRate = jack_get_sample_rate(g_jackClient); g_dFramesPerClock = getFramesPerClock(g_dTempo); // Register JACK callbacks - jack_set_process_callback(g_pJackClient, onJackProcess, 0); - jack_set_sample_rate_callback(g_pJackClient, onJackSampleRateChange, 0); + jack_set_process_callback(g_jackClient, onJackProcessChan, 0); + jack_set_sample_rate_callback(g_jackClient, onJackSampleRateChange, 0); // jack_set_xrun_callback(g_pJackClient, onJackXrun, 0); //!@todo Remove xrun handler (just for debug) - if (jack_activate(g_pJackClient)) { + if (jack_activate(g_jackClient)) { fprintf(stderr, "libzynseq cannot activate client\n"); return; } @@ -1344,7 +1347,7 @@ void setHorizontalZoom(uint16_t zoom) { g_nHorizontalZoom = zoom; } // Schedule a MIDI message to be sent in next JACK process cycle void sendMidiMsg(MIDI_MESSAGE* pMsg) { // Find first available time slot - uint32_t time = jack_frames_since_cycle_start(g_pJackClient); + uint32_t time = jack_frames_since_cycle_start(g_jackClient); while (g_bMutex) std::this_thread::sleep_for(std::chrono::milliseconds(1)); g_bMutex = true; @@ -2202,15 +2205,15 @@ bool isSolo(uint8_t bank, uint8_t sequence, uint32_t track) { void setTransportToStartOfBar() { jack_position_t position; - jack_transport_query(g_pJackClient, &position); + jack_transport_query(g_jackClient, &position); position.beat = 1; position.tick = 0; // position.valid = JackPositionBBT; - jack_transport_reposition(g_pJackClient, &position); + jack_transport_reposition(g_jackClient, &position); // g_pNextTimebaseEvent = g_pTimebase->getPreviousTimebaseEvent(position.bar, 1, TIMEBASE_TYPE_ANY); //!@todo Might miss event if 2 at start of bar } -void transportLocate(uint32_t frame) { jack_transport_locate(g_pJackClient, frame); } +void transportLocate(uint32_t frame) { jack_transport_locate(g_jackClient, frame); } /* Calculate the song position in frames from BBT */ @@ -2251,12 +2254,12 @@ jack_nframes_t transportGetLocation(uint32_t bar, uint32_t beat, uint32_t tick) } bool transportRequestTimebase() { - if (jack_set_timebase_callback(g_pJackClient, 0, onJackTimebase, NULL)) + if (jack_set_timebase_callback(g_jackClient, 0, onJackTimebase, NULL)) return false; return true; } -void transportReleaseTimebase() { jack_release_timebase(g_pJackClient); } +void transportReleaseTimebase() { jack_release_timebase(g_jackClient); } void transportStart(const char* client) { if (strcmp("zynseq", client)) { @@ -2265,11 +2268,11 @@ void transportStart(const char* client) { g_setTransportClient.emplace(client); } jack_position_t pos; - if (jack_transport_query(g_pJackClient, &pos) != JackTransportRolling) - jack_transport_start(g_pJackClient); + if (jack_transport_query(g_jackClient, &pos) != JackTransportRolling) + jack_transport_start(g_jackClient); if (g_nClockSource & TRANSPORT_CLOCK_INTERNAL) { // Send MIDI start message - jack_nframes_t nClockTime = g_qClockPos.front().first - jack_last_frame_time(g_pJackClient); + jack_nframes_t nClockTime = g_qClockPos.front().first - jack_last_frame_time(g_jackClient); g_mSchedule.insert(std::pair(nClockTime, new MIDI_MESSAGE({MIDI_START, 0, 0}))); } } @@ -2277,7 +2280,7 @@ void transportStart(const char* client) { void transportStop(const char* client) { if (strcmp(client, "ALL") == 0) { g_setTransportClient.clear(); - jack_transport_stop(g_pJackClient); + jack_transport_stop(g_jackClient); return; } auto itClient = g_setTransportClient.find(std::string(client)); @@ -2285,10 +2288,10 @@ void transportStop(const char* client) { g_setTransportClient.erase(itClient); g_bClientPlaying = (g_setTransportClient.size() != 0); if (!g_bClientPlaying && g_nPlayingSequences == 0) - jack_transport_stop(g_pJackClient); + jack_transport_stop(g_jackClient); if (g_nClockSource & TRANSPORT_CLOCK_INTERNAL) { // Send MIDI stop message - jack_nframes_t nClockTime = g_qClockPos.front().first - jack_last_frame_time(g_pJackClient); + jack_nframes_t nClockTime = g_qClockPos.front().first - jack_last_frame_time(g_jackClient); g_mSchedule.insert(std::pair(nClockTime, new MIDI_MESSAGE({MIDI_STOP, 0, 0}))); } } @@ -2303,7 +2306,7 @@ void transportToggle(const char* client) { uint8_t transportGetPlayStatus() { jack_position_t position; // Not used but required to query transport jack_transport_state_t nState; - return jack_transport_query(g_pJackClient, &position); + return jack_transport_query(g_jackClient, &position); } void setTempo(double tempo) { @@ -2324,7 +2327,7 @@ void setBeatsPerBar(uint32_t beats) { uint32_t getBeatsPerBar() { return g_nBeatsPerBar; } -void transportSetSyncTimeout(uint32_t timeout) { jack_set_sync_timeout(g_pJackClient, timeout); } +void transportSetSyncTimeout(uint32_t timeout) { jack_set_sync_timeout(g_jackClient, timeout); } void enableMetronome(bool enable) { g_bMetronome = enable; diff --git a/zynlibs/zynsmf/zynsmf.cpp b/zynlibs/zynsmf/zynsmf.cpp index 2a1e004cf..a0e068998 100644 --- a/zynlibs/zynsmf/zynsmf.cpp +++ b/zynlibs/zynsmf/zynsmf.cpp @@ -22,7 +22,7 @@ enum playState { STOPPING = 3 }; -jack_client_t* g_pJackClient = NULL; +jack_client_t* g_chanJackClient = NULL; jack_port_t* g_pMidiInputPort = NULL; jack_port_t* g_pMidiOutputPort = NULL; @@ -273,7 +273,7 @@ static int onJackSamplerate(jack_nframes_t nFrames, void* args) { } // Handle JACK process callback -static int onJackProcess(jack_nframes_t nFrames, void* notused) { +static int onJackProcessChan(jack_nframes_t nFrames, void* notused) { static uint8_t nCommand; static uint8_t nData1; static uint8_t nData2; @@ -286,8 +286,8 @@ static int onJackProcess(jack_nframes_t nFrames, void* notused) { static jack_position_t transport_position; static double dBeatsPerMinute = 120.0; - jack_nframes_t nNow = jack_last_frame_time(g_pJackClient); - jack_transport_state_t nTransportState = jack_transport_query(g_pJackClient, &transport_position); + jack_nframes_t nNow = jack_last_frame_time(g_chanJackClient); + jack_transport_state_t nTransportState = jack_transport_query(g_chanJackClient, &transport_position); // Handle change of tempo if (nPreviousTransportState != nTransportState || transport_position.beats_per_minute != dBeatsPerMinute && transport_position.beats_per_minute > 0) { @@ -470,17 +470,17 @@ static int onJackProcess(jack_nframes_t nFrames, void* notused) { } void removeJackClient() { - if (g_pJackClient) - jack_client_close(g_pJackClient); - g_pJackClient = NULL; + if (g_chanJackClient) + jack_client_close(g_chanJackClient); + g_chanJackClient = NULL; } bool createJackClient() { - if (!g_pJackClient) { + if (!g_chanJackClient) { // Initialise JACK client - g_pJackClient = jack_client_open("zynsmf", JackNoStartServer, NULL); - if (g_pJackClient && !jack_set_process_callback(g_pJackClient, onJackProcess, 0) && - !jack_set_sample_rate_callback(g_pJackClient, onJackSamplerate, 0) && !jack_activate(g_pJackClient)) { + g_chanJackClient = jack_client_open("zynsmf", JackNoStartServer, NULL); + if (g_chanJackClient && !jack_set_process_callback(g_chanJackClient, onJackProcessChan, 0) && + !jack_set_sample_rate_callback(g_chanJackClient, onJackSamplerate, 0) && !jack_activate(g_chanJackClient)) { fprintf(stderr, "Started libzynsmf\n"); return true; } @@ -496,7 +496,7 @@ bool attachPlayer(Smf* pSmf) { if (!createJackClient()) return false; if (!g_pMidiOutputPort) - g_pMidiOutputPort = jack_port_register(g_pJackClient, "midi_out", JACK_DEFAULT_MIDI_TYPE, JackPortIsOutput, 0); + g_pMidiOutputPort = jack_port_register(g_chanJackClient, "midi_out", JACK_DEFAULT_MIDI_TYPE, JackPortIsOutput, 0); if (!g_pMidiOutputPort) { removePlayer(); DPRINTF("Failed to create JACK output port\n"); @@ -504,7 +504,7 @@ bool attachPlayer(Smf* pSmf) { } DPRINTF("Created new JACK player\n"); g_pPlayerSmf = pSmf; - g_nSamplerate = jack_get_sample_rate(g_pJackClient); + g_nSamplerate = jack_get_sample_rate(g_chanJackClient); onJackSamplerate(g_nSamplerate, 0); // Set g_dTicksPerFrame return true; @@ -512,7 +512,7 @@ bool attachPlayer(Smf* pSmf) { void removePlayer() { if (g_pMidiOutputPort) - jack_port_unregister(g_pJackClient, g_pMidiOutputPort); + jack_port_unregister(g_chanJackClient, g_pMidiOutputPort); g_pMidiOutputPort = NULL; if (!g_pRecorderSmf) removeJackClient(); @@ -522,7 +522,7 @@ void removePlayer() { void setLoop(bool bLoop) { g_bLoop = bLoop; } void startPlayback() { - if (!g_pJackClient) + if (!g_chanJackClient) return; g_dPosition = 0.0; g_nPlayState = STARTING; @@ -544,7 +544,7 @@ bool attachRecorder(Smf* pSmf) { if (!createJackClient()) return false; if (!g_pMidiInputPort) - g_pMidiInputPort = jack_port_register(g_pJackClient, "midi_in", JACK_DEFAULT_MIDI_TYPE, JackPortIsInput, 0); + g_pMidiInputPort = jack_port_register(g_chanJackClient, "midi_in", JACK_DEFAULT_MIDI_TYPE, JackPortIsInput, 0); if (!g_pMidiInputPort) { removeRecorder(); DPRINTF("Failed to create JACK input port\n"); @@ -552,14 +552,14 @@ bool attachRecorder(Smf* pSmf) { } DPRINTF("Created new JACK recorder\n"); g_pRecorderSmf = pSmf; - g_nSamplerate = jack_get_sample_rate(g_pJackClient); + g_nSamplerate = jack_get_sample_rate(g_chanJackClient); onJackSamplerate(g_nSamplerate, 0); // Set g_dRecorderTicksPerFrame return true; } void removeRecorder() { if (g_pMidiInputPort) - jack_port_unregister(g_pJackClient, g_pMidiInputPort); + jack_port_unregister(g_chanJackClient, g_pMidiInputPort); g_pMidiInputPort = NULL; if (!g_pPlayerSmf) removeJackClient(); @@ -578,7 +578,7 @@ void startRecording() { g_aRecNotes[ch][note] = false; jack_position_t transport_position; - jack_transport_state_t nTransportState = jack_transport_query(g_pJackClient, &transport_position); + jack_transport_state_t nTransportState = jack_transport_query(g_chanJackClient, &transport_position); double dBeatsPerMinute = transport_position.beats_per_minute; g_nMicrosecondsPerQuarterNote = 60000000.0 / dBeatsPerMinute; g_dRecorderTicksPerFrame = double(g_pRecorderSmf->getTicksPerQuarterNote()) / ((double(g_nMicrosecondsPerQuarterNote) / 1000000) * double(g_nSamplerate)); @@ -597,7 +597,7 @@ void stopRecording() { uint8_t* pData = new uint8_t[2]; *pData = note; *(pData + 1) = 0; - uint32_t nPosition = jack_frame_time(g_pJackClient) - g_nRecordStartPosition; // Time of event in samples since start of recording + uint32_t nPosition = jack_frame_time(g_chanJackClient) - g_nRecordStartPosition; // Time of event in samples since start of recording Event* pEvent = new Event(g_dRecorderTicksPerFrame * nPosition, EVENT_TYPE_MIDI, MIDI_NOTE_OFF, 2, pData); g_pRecorderSmf->addEvent(chan, pEvent); // Add event to a track based on its MIDI channel }