Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes issues 1164 & 1168: switching to a new pattern was wrong; invalid LEDs status on mute/solo #508

Open
wants to merge 7 commits into
base: vangelis
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
.*~
__pycache__
zynlibs/*/build
.vscode/launch.json
.vscode
.idea
153 changes: 151 additions & 2 deletions zynconf/zynthian_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
from time import sleep
from stat import S_IWUSR
from shutil import copyfile
from subprocess import check_output
from subprocess import check_output, DEVNULL

# -------------------------------------------------------------------------------
# Configure logging
Expand Down Expand Up @@ -170,11 +170,160 @@
sys_dir = os.environ.get('ZYNTHIAN_SYS_DIR', "/zynthian/zynthian-sys")
config_dir = os.environ.get('ZYNTHIAN_CONFIG_DIR', '/zynthian/config')
config_fpath = config_dir + "/zynthian_envars.sh"
zynthian_repositories = ["zynthian-sys", "zynthian-ui", "zyncoder", "zynthian-data", "zynthian-webconf"]

# -------------------------------------------------------------------------------
# Config management
# Version configuration
# -------------------------------------------------------------------------------

def is_git_behind(path):
# True if the git repository is behind the upstream version
check_output(f"git -C {path} remote update; git -C {path} status --porcelain -bs | grep behind | wc -l",
encoding="utf-8", shell=True) != '0\n'

def get_git_branch(path):
# Get the current branch for a git repository or None if detached or invalid repo name
try:
return check_output(f"git -C {path} symbolic-ref -q --short HEAD",
encoding="utf-8", shell=True).strip()
except:
return None

def get_git_tag(path):
# Get the current tag for a git repository or None if invalid repo name
try:
status = check_output(f"git -C {path} status",
encoding="utf-8", shell=True, stderr=DEVNULL).split('\n')
for line in status:
if line.strip().startswith("HEAD detached at "):
return line.strip()[17:]
except:
return None

def get_git_local_hash(path):
# Get the hash of the current commit for a git branch or None if invalid
try:
return check_output(f"git -C {path} rev-parse HEAD",
encoding="utf-8", shell=True).strip()
except:
return None

def get_git_remote_hash(path, branch=None):
# Get the hash of the latest commit for a git branch or for a tag or None if invalid
if branch is None:
branch = get_git_tag(path)
if branch is None:
return None
try:
return check_output(f"git -C {path} ls-remote origin {branch}",
encoding="utf-8", shell=True).strip().split("\t")[0]
except:
return None

def get_git_version_info(path):
# Get version information about a git repository
local_hash = get_git_local_hash(path)
branch = get_git_branch(path)
tag = get_git_tag(path)
release_name = None
version = None
major_version = 0
minor_version = 0
patch_version = 0
frozen = False
if tag is not None:
# Check if it is a major release channel
parts = tag.split("-", 1)
if len(parts) == 2:
release_name = parts[0]
version = parts[1]
if version:
parts = version.split(".", 3)
major_version = parts[0]
if len(parts) > 2:
patch_version = parts[2]
if len(parts) > 1:
minor_version = parts[1]
frozen = True
else:
# On stable release channel. Check which point release we are on.
tags = check_output(f"git -C {path} tag --points-at {tag}", encoding="utf-8", shell=True).split()
for t in tags:
parts = t.split("-", 1)
if len(parts) != 2 or parts[0] != release_name:
continue
v_parts = parts[1].split(".", 3)
try:
major_version = int(major_version)
x = int(v_parts[0])
y = z = 0
if len(v_parts) > 1:
y = int(v_parts[1])
if len(v_parts) > 2:
z = int(v_parts[2])
if x > major_version:
major_version = x
minor_version = y
patch_version = z
elif y > minor_version:
minor_version = y
patch_version = z
elif z > patch_version:
patch_version = z
except:
pass
result = {
"branch": branch,
"tag": tag,
"name": release_name,
"major": major_version,
"minor": minor_version,
"patch": patch_version,
"frozen": frozen,
"local_hash": local_hash
}
return result

def update_git(path):
check_output(f"git -C {path} remote update origin --prune", shell=True)

def get_git_tags(path, refresh=False):
# Get list of tags in a git repository
try:
if refresh:
update_git(path)
return sorted(check_output(f"git -C {path} tag", encoding="utf-8", shell=True).split(), key=str.casefold)
except:
return []

def get_git_branches(path, refresh=False):
# Get list of branches in a git repository
result = []
if refresh:
update_git(path)
for branch in check_output(f"git -C {path} branch -a", encoding="utf-8", shell=True).splitlines():
branch = branch.strip()
if branch.startswith("*"):
branch = branch[2:]
if branch.startswith("remotes/origin/"):
branch = branch[15:]
if "->" in branch or branch.startswith("(HEAD detached at "):
continue
if branch not in result:
result.append(branch)
return sorted(result, key=str.casefold)

def get_system_version():
# Get the overall release version or None if inconsistent repository states
tag = get_git_version_info("/zynthian/zynthian-sys")["tag"]
for repo in zynthian_repositories:
if get_git_version_info(f"/zynthian/{repo}")["tag"] != tag:
return None
return tag

# -------------------------------------------------------------------------------
# Config management
# -------------------------------------------------------------------------------

def get_midi_config_fpath(fpath=None):
if not fpath:
Expand Down
65 changes: 35 additions & 30 deletions zyngine/ctrldev/zynthian_ctrldev_akai_apc_key25_mk2.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,8 +188,7 @@ def __init__(self, state_manager, idev_in, idev_out=None):
self._device_handler = DeviceHandler(state_manager, self._leds)
self._mixer_handler = MixerHandler(state_manager, self._leds)
self._padmatrix_handler = PadMatrixHandler(state_manager, self._leds)
self._stepseq_handler = StepSeqHandler(
state_manager, self._leds, idev_in)
self._stepseq_handler = StepSeqHandler(state_manager, self._leds, idev_in)
self._current_handler = self._mixer_handler
self._is_shifted = False

Expand Down Expand Up @@ -712,19 +711,19 @@ def refresh(self):
return

query = {
FN_MUTE: self._zynmixer.get_mute,
FN_SOLO: self._zynmixer.get_solo,
FN_SELECT: self._is_active_chain,
FN_MUTE: lambda c: self._zynmixer.get_mute(c.mixer_chan),
FN_SOLO: lambda c: self._zynmixer.get_solo(c.mixer_chan),
FN_SELECT: lambda c: c.chain_id == self._active_chain,
}[self._track_buttons_function]
for i in range(8):
index = i + (8 if self._chains_bank == 1 else 0)
chain = self._chain_manager.get_chain_by_index(index)
pos = i + (8 if self._chains_bank == 1 else 0)
chain = self._chain_manager.get_chain_by_position(pos)
if not chain:
break
# Main channel ignored
if chain.chain_id == 0:
continue
self._leds.led_state(BTN_TRACK_1 + i, query(index))
self._leds.led_state(BTN_TRACK_1 + i, query(chain))

def on_shift_changed(self, state):
retval = super().on_shift_changed(state)
Expand Down Expand Up @@ -786,10 +785,18 @@ def cc_change(self, ccnum, ccval):
def update_strip(self, chan, symbol, value):
if {"mute": FN_MUTE, "solo": FN_SOLO}.get(symbol) != self._track_buttons_function:
return
chan -= self._chains_bank * 8
if 0 > chan > 8:

# Mixer 'chan' may not be equal to its position (if re-arranged or a
# chain was deleted). Search the actual displayed position.
chain_id = self._chain_manager.get_chain_id_by_mixer_chan(chan)
for pos in range(self._chain_manager.get_chain_count()):
if self._chain_manager.get_chain_id_by_index(pos) == chain_id:
break

pos -= self._chains_bank * 8
if 0 > pos > 8:
return
self._leds.led_state(BTN_TRACK_1 + chan, value)
self._leds.led_state(BTN_TRACK_1 + pos, value)
return True

def set_active_chain(self, chain, refresh):
Expand All @@ -801,12 +808,6 @@ def set_active_chain(self, chain, refresh):
if refresh:
self.refresh()

def _is_active_chain(self, position):
chain = self._chain_manager.get_chain_by_position(position)
if chain is None:
return False
return chain.chain_id == self._active_chain

def _update_volume(self, ccnum, ccval):
return self._update_control("level", ccnum, ccval, 0, 100)

Expand Down Expand Up @@ -934,8 +935,11 @@ def __init__(self, state_manager, leds: FeedbackLEDs):

def on_record_changed(self, state):
self._is_record_pressed = state
if state and self._recording_seq:
self._stop_pattern_record()

# Only STOP recording allowed, as START conflicts with RECORD + PAD
if state and self._recording_seq is not None:
if self._libseq.isMidiRecord():
self._stop_pattern_record()

def on_toggle_play(self):
self._state_manager.send_cuia("TOGGLE_PLAY")
Expand Down Expand Up @@ -1185,25 +1189,28 @@ def _refresh_tool_buttons(self):
return

# If seqman is disabled, show playing status in row launchers
playing_rows = {seq %
self._zynseq.col_in_bank for seq in self._playing_seqs}
playing_rows = {
seq % self._zynseq.col_in_bank for seq in self._playing_seqs}
for row in range(5):
state = row in playing_rows
self._leds.led_state(BTN_SOFT_KEY_START + row, state)

def _start_pattern_record(self, seq):
# Set pad's chain as active
channel = self._libseq.getChannel(self._zynseq.bank, seq, 0)
chain_id = self._chain_manager.get_chain_id_by_mixer_chan(channel)
if chain_id is None:
return

if self._libseq.isMidiRecord():
self._state_manager.send_cuia("TOGGLE_RECORD")
self._chain_manager.set_active_chain_by_id(chain_id)

self._show_pattern_editor(seq)
# Open Pattern Editor
self._show_pattern_editor(seq, skip_arranger=True)

# Start playing & recording
if self._libseq.getPlayState(self._zynseq.bank, seq) == zynseq.SEQ_STOPPED:
self._libseq.togglePlayState(self._zynseq.bank, seq)
self._state_manager.send_cuia("TOGGLE_PLAY")
if not self._libseq.isMidiRecord():
self._state_manager.send_cuia("TOGGLE_RECORD")

Expand Down Expand Up @@ -1289,7 +1296,7 @@ def _copy_sequence(self, src_scene, src_seq, dst_scene, dst_seq):

# Also copy StepSeq instrument pages
self._request_action("stepseq", "sync-sequences",
src_scene, src_seq, dst_scene, dst_seq)
src_scene, src_seq, dst_scene, dst_seq)


# --------------------------------------------------------------------------
Expand Down Expand Up @@ -1655,7 +1662,7 @@ def __init__(self, state_manager, leds: FeedbackLEDs, dev_idx):
self._is_arranger_mode = False

# We need to receive clock though MIDI
# TODO: Changing clock source from user preference seems wrong!
# FIXME: Changing clock source from user preference seems wrong!
self._state_manager.set_transport_clock_source(1)

# Pads ordered for cursor sliding + note pads
Expand Down Expand Up @@ -1849,8 +1856,7 @@ def note_on(self, note, velocity, shifted_override=None):
return True

if note == BTN_PLAY:
self._libseq.togglePlayState(
self._zynseq.bank, self._selected_seq)
self._libseq.togglePlayState(self._zynseq.bank, self._selected_seq)

elif BTN_PAD_START <= note <= BTN_PAD_END:
self._pressed_pads[note] = time.time()
Expand Down Expand Up @@ -2059,8 +2065,7 @@ def _update_step_velocity(self, step, delta):
velocity = self._libseq.getNoteVelocity(step, note) + delta
velocity = min(127, max(10, velocity))
self._libseq.setNoteVelocity(step, note, velocity)
self._leds.led_on(self._pads[step], COLOR_RED,
int((velocity * 6) / 127))
self._leds.led_on(self._pads[step], COLOR_RED, int((velocity * 6) / 127))
self._play_step(step)

def _update_step_stutter_count(self, step, delta):
Expand Down
4 changes: 1 addition & 3 deletions zyngine/ctrldev/zynthian_ctrldev_akai_mpk_mini_mk3.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,7 @@
from zyncoder.zyncore import lib_zyncore
from zyngine.zynthian_signal_manager import zynsigman

from .zynthian_ctrldev_base import (
zynthian_ctrldev_zynmixer
)
from .zynthian_ctrldev_base import zynthian_ctrldev_zynmixer
from .zynthian_ctrldev_base_extended import (
CONST, KnobSpeedControl, IntervalTimer, ButtonTimer
)
Expand Down
2 changes: 1 addition & 1 deletion zyngine/ctrldev/zynthian_ctrldev_base_extended.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ def _run_callback(self, note, elapsed):


# --------------------------------------------------------------------------
# Helper class to handle knobs' speed
# Helper class to handle knobs' speed
# --------------------------------------------------------------------------
class KnobSpeedControl:
def __init__(self, steps_normal=3, steps_shifted=8):
Expand Down
13 changes: 10 additions & 3 deletions zyngine/ctrldev/zynthian_ctrldev_base_ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ def _show_pattern_editor(self, seq=None, skip_arranger=False):
self._state_manager.send_cuia("SCREEN_ZYNPAD")
if seq is not None:
self._select_pad(seq)
self._refresh_pattern_editor()
if not skip_arranger:
zynthian_gui_config.zyngui.screens["zynpad"].show_pattern_editor()
else:
Expand All @@ -56,9 +57,15 @@ def _select_pad(self, pad):
# This SHOULD not be coupled to UI! This is needed because when the pattern is changed in
# zynseq, it is not reflected in pattern editor.
def _refresh_pattern_editor(self):
index = self._zynseq.libseq.getPatternIndex()
zynthian_gui_config.zyngui.screens["pattern_editor"].load_pattern(
index)
zynpad = zynthian_gui_config.zyngui.screens["zynpad"]
patted = zynthian_gui_config.zyngui.screens['pattern_editor']
pattern = self._zynseq.libseq.getPattern(zynpad.bank, zynpad.selected_pad, 0, 0)

self._state_manager.start_busy("load_pattern", f"loading pattern {pattern}")
patted.bank = zynpad.bank
patted.sequence = zynpad.selected_pad
patted.load_pattern(pattern)
self._state_manager.end_busy("load_pattern")

# FIXME: This SHOULD not be coupled to UI!
def _get_selected_sequence(self):
Expand Down
Loading