From f03698ca86720321dd4d9603a6ab237784462b05 Mon Sep 17 00:00:00 2001 From: melancholytron Date: Tue, 9 Sep 2025 14:35:20 -0500 Subject: [PATCH] Fix group cycling with note-based timing and GUI scaling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Major improvements to preset group functionality: - Replace timer-based cycling with accurate note counting via pattern_step signal - Group cycling now counts actual notes played (pattern_length × loop_count) - Add GUI scaling support for dynamic button sizing on different resolutions - Implement complete preset group UI with add/remove, manual controls, and status - Add master file save/load functionality for preset groups - Fix scale_note_start not saving in presets - Update button styling across all controls for consistency 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .claude/settings.local.json | 17 +- __pycache__/main.cpython-310.pyc | Bin 3011 -> 2277 bytes .../arpeggiator_engine.cpython-310.pyc | Bin 16822 -> 25828 bytes .../output_manager.cpython-310.pyc | Bin 10670 -> 10670 bytes .../volume_pattern_engine.cpython-310.pyc | Bin 8699 -> 9552 bytes .../arpeggiator_controls.cpython-310.pyc | Bin 15040 -> 27523 bytes .../channel_controls.cpython-310.pyc | Bin 7285 -> 7346 bytes gui/__pycache__/main_window.cpython-310.pyc | Bin 18694 -> 21554 bytes .../output_controls.cpython-310.pyc | Bin 8092 -> 8131 bytes .../preset_controls.cpython-310.pyc | Bin 13754 -> 31973 bytes .../simulator_display.cpython-310.pyc | Bin 6083 -> 6134 bytes .../volume_controls.cpython-310.pyc | Bin 8522 -> 10182 bytes gui/arpeggiator_controls.py | 167 ++++- gui/main_window.py | 96 ++- gui/preset_controls.py | 627 +++++++++++++++++- gui/volume_controls.py | 119 ++-- master_files/example_master.json | 129 ++++ master_files/test master.json | 193 ++++++ presets/{butt 2.json => two Copy Copy.json} | 21 +- presets/two Copy.json | 58 ++ presets/two.json | 58 ++ 21 files changed, 1395 insertions(+), 90 deletions(-) create mode 100644 master_files/example_master.json create mode 100644 master_files/test master.json rename presets/{butt 2.json => two Copy Copy.json} (72%) create mode 100644 presets/two Copy.json create mode 100644 presets/two.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 342a06f..b84f5e8 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -3,9 +3,22 @@ "allow": [ "Bash(python:*)", "Bash(copy guiarpeggiator_controls.py guiarpeggiator_controls_backup.py)", - "Bash(copy guiarpeggiator_controls_new.py guiarpeggiator_controls.py)" + "Bash(copy guiarpeggiator_controls_new.py guiarpeggiator_controls.py)", + "Bash(git add:*)", + "Bash(git commit:*)", + "Bash(git push:*)", + "Bash(git reset:*)", + "Bash(grep:*)", + "Bash(dir)", + "Bash(run_in_venv.bat)", + "Bash(./run_in_venv.bat)", + "Bash(git rm:*)", + "Bash(del test_group_cycling.py)" ], "deny": [], - "ask": [] + "ask": [], + "additionalDirectories": [ + "C:\\c\\git" + ] } } \ No newline at end of file diff --git a/__pycache__/main.cpython-310.pyc b/__pycache__/main.cpython-310.pyc index a5651f504420ac0f2fabf86b51c930c5a88708a1..8cce9eac64992739938e0d7e8621fea974019cba 100644 GIT binary patch delta 398 zcmX>s{#1}JpO=@50SG?x?awge-pIFwiSfYX!%T~{Y8bM_vm_Qs0%@s*jEoE*{sQR~ zMi4uNA(%mv$?v5AP@|^I z#FEq^;mI@DH64V2LQ#AWq49L!b5UuY-{dY%4|y4&O0cO#av&DMIxtIm r@<&cFW?dex&0Jg&jH>b=B_OkJv7}aHmf+CR&TYzW!^SAY$np;WY8Fxe delta 1144 zcmZ`%OK;Oa5cclcj%yM(P180heQ>F&pb`}%1c#~uQA7`b5Iuk*sIly&apc(PuG0r{ z6hygj1Np`&5ho-r2o4;OK;i;_!8Z;Z`X>;xPFo%-mS#Mj@0)MNyYpk@%Y+f+a=HZ1 zXZ=%sz3|p}kI~oo3x0JjEbU2=WR@eTQEp83PQrVtj}YV<<=(V|MC?dwQU%}K&x1f^ zCVo!lQ4)V3*TF0DjoE~78YZO{qg!s!=B8zb?iMxey5;-Smaa3uFJk~ZhyJ0GEv)KMY@>dR?; zH>Eg=poAkpDKthMbq}-kLEI?z#v<%wQi-b(>W%ls>E#`5O$?Vm68fLclVfz;Fq7T^ zsDO-+stm9ObTPP+^yutm)}kBnf?8UwE_s1%d3+gyL+XAP zX#0*37~yWS?O9>KOvmLd&)VHDLjGoDf{hDYZMPgNq^uZ!R4Uz3NY#u%%4}I706r7{ z!U_V(9c<2E)^XXKE@8EC2ijh=v*! diff --git a/core/__pycache__/arpeggiator_engine.cpython-310.pyc b/core/__pycache__/arpeggiator_engine.cpython-310.pyc index 8d587d4c068ea5ca93e0721cd69e4e62c24adfa8..6b12e36018699292f52396be548cb2cb6718a232 100644 GIT binary patch literal 25828 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 R0NZ;TJf!G7#lJE0{{Yspx^Dmg delta 6624 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 diff --git a/core/__pycache__/volume_pattern_engine.cpython-310.pyc b/core/__pycache__/volume_pattern_engine.cpython-310.pyc index a3ccb08a7689e5f59a7a8ad8356349eba5219d89..75bc4a2fc15df723833fb2748ef1439b4fea7f01 100644 GIT binary patch literal 9552 zcmb7KOK%)kcCJ@fR~MVj=0gaj(=C67n8=#gZNld&vY8c*DjQ)%`sl0{WN ze5)v#o#M;@QsMxUMYah7L`r}l{s{pBWRX?YSp+F$5g-dgn_vR3lEoz7IrZpnsyRZM z=+pO}`#k5K*FBfL>1kWR=fD2)A2x1XR+Rss%IL>HWd+~xE`U&)icp0Xs7;lBwWfw& zJsNhY4^I3 z>;=whFWPWgTf*--frlkSht@78U6nL_*9$T43$RAH(=Za_rr(YdN z^4`8D-h5hW9svF=;Ar1zTY1eAXT&nv4vt`nnzP~@W;(PBdtO`s_V6z3MR5t(*LGnq ziz~n$*@e9--Ujw)w!TAWlhO8l@eW#!i64k-PnG6z@ve9ezbC#{45fJz!oQNtr?Q`k zek%EczqTEGZl^N+n-t!~?cVx)p!$UDt*BNgW`mFJH>y!oNiM)$54ay&y=Y&bzBE>1LG+qm@gemA6MK z)$>clu-}VV(Ua-b-WFKvrq-tojVwMIN@N^$ ze8WEhh?FPFGxfPbrY}}pO=y5RLqq5&mxKXmiV|SiR{$-B6=9-mi!$I8W2y|Nqna=T zH2tRX#l9Uint3+!ToIK8C8}@EE@1p-HCC@FDIT?Hs(raE6v&o+N0K4=7GHdRbv-L6 z$_t|5g$2;izWwt*qt8EGm&dsN;xx7WoWLvrBC#=t6W;2BUzY3oT|aW&WTw3(B^^~} zIg{zE;D&wA6Ul5|^}CVR4PmO2dXDP%P=`Srb`rjXO(c{}5)^pd^=Kp6mq}^HdKYK2 z?uE%rZv1n^{Cdz^Z3Sc9l6^Tc^Au_4q*iQVqZMMZ>GZ%|l4Mj%yoNjisuQEr!X~u7 z|BaRyp%<({TvE09MolWN+YVY`=(^u1|8wrfI}g_V=s`>N``)_WihA-vyC=N|sm$a& zazPgS+H!wezKI2n;u{_ZuvJqxRs0PhmGG_UOKM#;emyum!AdL_hf&Rmf=?qE_M$D> zZPU@^A1afS-oQ6J2@pd*Hdoq*nh&Z+mc`&?=A2l?24XyHr1Xc?0K1#_{ z=oP~BE)XHbB(`R%Rn@@X;OHa{jEsSZlHk2WbqUiD)!!`wM4&A~@Di&}v?rvSn$SU~ zF=!B-*`6Jq$mwhak>3w|Pdca1EuVLyo^yHmTw^^|n7_Nej`{FeSh>^C6TQ>=3c&Bm zGr;9^{s`sWbk2_KIn|Y0==B{P(4sat^$IF8?Wz=7wI{vuL)wTjI5SFFF>Dug=ZLxs zpiUmgsN7HxW%9RxkiSh}nI_Cm@D3qX3cx40Z276>Y_s#7r%~D~Y>E z#KB@9JR^(YoX(FhtvAwGFbx~Crcj>1W?s|*#>qPmOitb>DDNh(M&u2x6OokCb$l6^%WJ`N&)!n zmxwu&%r$ZHI}@O;)zvw5@YY_;nlw1$n3r?R+nZzZGpqq|KhN~%w}V%3;LQ08R~u3# zQr-X@n^8Oll2=|yGU88fJ&ivd zTG=@rZg<-ove)$o-U!v~>hx4?1y+?Fft3QVRhu8D1rf5>@1hH(79#vdUZEXrT&tq?C=<%)mV3aE%wDWK!|6X)A80%J4lYit*U;*nQj81KrnaE$ zlp-A=oq>?9#9px!>oJVrk`k8|l>N$%8Jl39K8X3w%C})8T($y|on0|4U3OcAe=OuE z-F7Z$>D`y+`IMvVVO9zk@t}v~(a^6JOAf6mjHOG=SaRF>Hgef=AGLxlkE5)inQfFu z91umELl-=}aq+;!Ouhc@V`PmTcdlEQC#(Y0N>$y~>uOy$w7s==d@%0A(6GV=jD;|C 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 YZD}Hsv;6M|&PVoFMM$Ul+`-BJ0emMLxBvhE diff --git a/gui/__pycache__/arpeggiator_controls.cpython-310.pyc b/gui/__pycache__/arpeggiator_controls.cpython-310.pyc index 5c07be7a883b453f35db160661721ff218c71cfb..db87535f5936afca50b2cc81c327d0d3277e390e 100644 GIT binary patch literal 27523 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 literal 15040 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 nQ9ZLw>GIhHcLEdZ864ynur0P zIS&#Olgd(M6|K5bHG zolco!*T~wdsp}Q%YB>+Dv5sE|$7R!@<);BSgEJL_i^~+JoSL)*1~;r4cGLDZyB(|6 zYMbdY>{_0dm$tLL(8Yyh+~Q-NEfch{%2<*dF(U<8H?*i1~*Zcu!#)uSw2D~Tjh41)68n|Kbrlbj^ zew#C8gSYCin_@0XycHbL^ZTZbyzZ$K3FS zZCEh`MyDY%ss=CyT?QX_n*|0;zzR13nxZ*xN!CUiv#;VqIc(D_@SFS`7V)to42Ag1 z5f%=xhUnGUskAUR%Swpxi4tsSr*sG51i?=@MhFlNGxV@K*~lQ+6vzj+T?YCI?3t0B zZh%hos{y{6t+PB%Ti?Yg<(ZuCw$JFS|WK`2nV!#!E8f*686v=82TOKv(sa7C&ZmTi%^nQUbh!Ibq ziu7`)TRNxdb(c;T%dKZtJ(mC)@yni?GHPyCWdFME=zJ>I2EWeB@#%7SQrV(dOiO1RpzSC70&Yd5Z0D@ z1KlJm{OjERPH?oe4nE>)>Aembx-JrEraH|=R|zy5{TzXssh=m1-E3YWmQ3k%e6E=j WsQZuezCxdR5TE(?LmWT(@B9HjZ{}?P delta 1082 zcmZ8fO=uHQ5Y8mo-TZ7eY1*2m4L0IWyIM6pSStRdRxCm(YOA)X8ftcHQ({v#+i2?+ zg(?bOgaJK>vOV@xG=d-^h}VkXF@gsX57nFW?#w1y=q~T$d-LYa%s2C19p30wilI=z zMc=z^@27t6da3LX;5DX%`LrFhT+@MN0GBCiz9q0+*^uS2-T$n09%2v6Z7Y^X^J?B@ z5A9&srTTG0$VV3BzS-Hre9|l?`w9g$j3qIL>*7KzNhy79A4EMqP(X$VF%f)~Md=wp z7smbLa0KW4L+#zY=!CdjiwPH3uqv^Md(`+7N*pjrDY&L#tyaK~`v?(3ox{VG7 z7HnL=JJFc9!t@fZG<}JrnI2@g$dJKD&52H@J}0`9=M0~0$#afutCi2qR{?tPK&> z&8|u%-5k+xnYFGTCzC4GCE!AA-}WsN*;Id?T;Enc#@eBeZ)Lnwc}-Wn&fSbWku&pU zJ)0}e&6u@jh&S`vb=}-r6>I!d*%|kPe?QS7HDDaXL#=!I)3mEAWHwHaX`levdgyX7 zpVLbQm!XQAO|P}M)&zXfnu02RZhhX*@5hiB_<py6uU385t#SGw>|TL(;GTjJ?o2Up~e diff --git a/gui/__pycache__/main_window.cpython-310.pyc b/gui/__pycache__/main_window.cpython-310.pyc index 86b00832a258e36887f46e2df3dd79bfda3c687c..bfb78bcef25f08359719d5d46f3ebc21c154529d 100644 GIT binary patch delta 5374 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 UXIYx?r@dJchM!40|7p*E0q<>(mH+?% diff --git a/gui/__pycache__/output_controls.cpython-310.pyc b/gui/__pycache__/output_controls.cpython-310.pyc index 893444d02c8d2ce4320a66b0bfd1400af32cebda..dbff333cdeea0868c2fe60febc4cd6b175c23772 100644 GIT binary patch delta 954 zcmZuuO-K}R6yLYw>dffu`nm3h>vk$PBvZ;ZNp#Ck(p=Fjwqo6OCSB24m>EH6MaV-C zcMfXx!sidD$i;u5jaoSdzsf7KDC zL`spBoM{QFv>h`=gY-tN7*2v4sFOy}-8 z-Qe#eLEe?t;|pn3izjCj`i!O;9mK})iL15zJXywB+;h313xBw_;eIY#?T2AjjDwy;dyHZ+*-Ys(^V-C0 z+B#gH`C^ws#lrd*iTi*bJv-iUvL>&D%8cVIJHZfQ7{fJx(9uWc7kuFjLll2|@6oN6 zeZkhF%S1_$ccgSxTPj_ppF-d8t?%iDt7LJHlB2W`eK_Sb*`);ri2I4V{$Fqlx0_DG zIDTq61(PTTu7$c;&#w~_3s;R*npVPEG~+4#PTIJ|(F8*j9|XbKkhZRK?SO2t7m! zEqD=oa1LH9q$(aN9?F785h{4CdQfr^4}wroJO~~O}lv1+Of88p8 zrC4Fg#H?i|p9_*IFM&#GQ!A;8sdbRXQF5yGZ8`_B%o~c9QoUN~2p25YumeYxVSKFg zf(t(>Js7dytZOBsvQ>r#Nj6^C*R0TPOgke0Y2!h?2UGeW^=GQ30VVb)%TFoS9A_x4hpsG2iQxsUkS7lkD5_AeeZUqqhB z=_)V3?4q5;ZwR63g@mqZ^ZyK*#7)27Hb5P=@T>o6_j!ugLe*~eQTopVUZJ?!!XV|o zl`jVVJ6Uli=o&re38|g diff --git a/gui/__pycache__/preset_controls.cpython-310.pyc b/gui/__pycache__/preset_controls.cpython-310.pyc index 1497be384e225b3ba77506c48b56c1426644c323..a5450d3d0f0f733cce4e9c642f775b69140bcc1c 100644 GIT binary patch literal 31973 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 aP2fIeK$a6@)<81$VtOJ)qa+s=vHu_T8p{6w delta 5758 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 KJ9Hdfz^y-G(}hp~ delta 436 zcmYk1JxIe)5XW=xH6Lw~wn@|Irjw!wer!63AcCTkrGu`XpsgU(EJ}()Ko=(uL=YvJ z)J+YzI5@aCC^{8!(M>wKh?~A(72k0`_}~5Cy?Y=1H~r?SX=WsGM%eYz=()KfXH}t+ z*aho%%dIw@_@Cfmam|JM#O(To`x)mfd6xqY_EVOX1zki33%&j5p$?wUaX-W zNQbO7;>T(IJ&?F;EF{bRy3i>}Jw>`uhC05)7n>lkyd>DnC-OFdlRzO&9Rru9)N$HV zYoJnE8>AVnH`giJ2Gf`UHn+JOZ)^7qCgZZs8O+k0`3{TmboLIQO6~bK*xI!oU?+aG zWPn|2*jM_#7*Jaf*yyHL%b~NvCp^=s(+5Y?a9&`ZE{p4MLIdtGtk9F&1rzk;e!%cI Dqo;4O diff --git a/gui/__pycache__/volume_controls.cpython-310.pyc b/gui/__pycache__/volume_controls.cpython-310.pyc index 324d20891144ea70de6b310f97ce17e3e27f72b5..08ee6b8d80d504451d22ca7d5a4844a1c961f395 100644 GIT binary patch delta 4323 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 10: display_name = display_name[:10] - btn = QPushButton(display_name) - btn.setFixedSize(120, 22) # Taller buttons for better readability - btn.setCheckable(True) - btn.setStyleSheet("background: #3a3a3a; color: #ffffff; font-size: 12px; font-weight: bold; padding: 0px; border: 1px solid #555555;") + btn = self.create_scalable_button(display_name, 120, 22, 12, checkable=True, + style_type="active" if scale == "major" else "normal") btn.clicked.connect(lambda checked, s=scale: self.on_scale_clicked(s)) if scale == "major": btn.setChecked(True) - btn.setStyleSheet("background: #00aa44; color: white; font-size: 12px; font-weight: bold; padding: 0px; border: 1px solid #00cc55;") self.scale_buttons[scale] = btn scales_layout.addWidget(btn, i // 4, i % 4) diff --git a/gui/main_window.py b/gui/main_window.py index 7b1f2a9..149e5cf 100644 --- a/gui/main_window.py +++ b/gui/main_window.py @@ -7,8 +7,9 @@ Integrates all GUI components into a cohesive interface. from PyQt5.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QGridLayout, QPushButton, QLabel, QSlider, QComboBox, - QSpinBox, QGroupBox, QTabWidget, QSplitter, QFrame) -from PyQt5.QtCore import Qt, QTimer, pyqtSlot + QSpinBox, QGroupBox, QTabWidget, QSplitter, QFrame, + QSizePolicy) +from PyQt5.QtCore import Qt, QTimer, pyqtSlot, QSize from PyQt5.QtGui import QFont, QPalette, QColor, QKeySequence from .arpeggiator_controls import ArpeggiatorControls @@ -18,6 +19,65 @@ from .simulator_display import SimulatorDisplay from .output_controls import OutputControls from .preset_controls import PresetControls +class ScalingUtils: + """Utility class for handling dynamic GUI scaling""" + + @staticmethod + def get_scale_factor(widget): + """Calculate scale factor based on widget size""" + # Base size is 1200x800 + base_width = 1200 + base_height = 800 + + # Get actual widget size + actual_size = widget.size() + width_factor = actual_size.width() / base_width + height_factor = actual_size.height() / base_height + + # Use the smaller factor to maintain proportions + scale_factor = min(width_factor, height_factor) + + # Clamp between 0.8 and 3.0 for reasonable bounds + return max(0.8, min(3.0, scale_factor)) + + @staticmethod + def scale_font_size(base_size, scale_factor): + """Scale font size with factor""" + return max(8, int(base_size * scale_factor)) + + @staticmethod + def scale_button_size(base_width, base_height, scale_factor): + """Scale button dimensions with factor""" + return ( + max(30, int(base_width * scale_factor)), + max(20, int(base_height * scale_factor)) + ) + + @staticmethod + def apply_scalable_button_style(button, base_font_size=12, scale_factor=1.0): + """Apply scalable styling to a button""" + font_size = ScalingUtils.scale_font_size(base_font_size, scale_factor) + padding = max(2, int(5 * scale_factor)) + + button.setStyleSheet(f""" + QPushButton {{ + background: #3a3a3a; + color: #ffffff; + font-size: {font_size}px; + font-weight: bold; + border: 1px solid #555555; + padding: {padding}px; + min-height: {max(18, int(22 * scale_factor))}px; + }} + QPushButton:hover {{ + background: #505050; + border: 1px solid #777777; + }} + """) + + # Set size policy to allow expansion + button.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + class MainWindow(QMainWindow): """ Main application window containing all GUI components. @@ -37,6 +97,10 @@ class MainWindow(QMainWindow): self.setWindowTitle("MIDI Arpeggiator - Lighting Controller") self.setMinimumSize(1200, 800) + # Scaling support + self.scale_factor = 1.0 + self.scaling_enabled = True + # Keyboard note mapping self.keyboard_notes = { Qt.Key_A: 60, # C @@ -66,6 +130,8 @@ class MainWindow(QMainWindow): # Create main layout with full-window tabs main_layout = QVBoxLayout(central_widget) + main_layout.setContentsMargins(8, 8, 8, 8) + main_layout.setSpacing(8) # Transport controls at top transport_frame = self.create_transport_controls() @@ -73,7 +139,8 @@ class MainWindow(QMainWindow): # Create tabbed interface that fills the window tab_widget = QTabWidget() - main_layout.addWidget(tab_widget) + tab_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + main_layout.addWidget(tab_widget, 1) # Give tab widget all extra space # Arpeggiator tab with quadrant layout self.arp_controls = ArpeggiatorControls(self.arpeggiator, self.channel_manager, self.simulator) @@ -622,6 +689,29 @@ class MainWindow(QMainWindow): super().keyReleaseEvent(event) + def resizeEvent(self, event): + """Handle window resize for dynamic scaling""" + super().resizeEvent(event) + + if self.scaling_enabled and hasattr(self, 'arp_controls'): + # Calculate new scale factor + new_scale_factor = ScalingUtils.get_scale_factor(self) + + # Only update if scale factor changed significantly + if abs(new_scale_factor - self.scale_factor) > 0.1: + self.scale_factor = new_scale_factor + self.update_scaling() + + def update_scaling(self): + """Update all GUI elements with new scaling""" + # Update controls with new scaling + if hasattr(self.arp_controls, 'apply_scaling'): + self.arp_controls.apply_scaling(self.scale_factor) + if hasattr(self.volume_controls, 'apply_scaling'): + self.volume_controls.apply_scaling(self.scale_factor) + if hasattr(self.preset_controls, 'apply_scaling'): + self.preset_controls.apply_scaling(self.scale_factor) + def closeEvent(self, event): """Handle window close event""" # Clean up resources diff --git a/gui/preset_controls.py b/gui/preset_controls.py index f930463..3c7ca31 100644 --- a/gui/preset_controls.py +++ b/gui/preset_controls.py @@ -7,10 +7,12 @@ Interface for saving, loading, and managing presets. from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QGridLayout, QGroupBox, QListWidget, QPushButton, QLineEdit, QLabel, QFileDialog, QMessageBox, QListWidgetItem, - QInputDialog, QFrame) -from PyQt5.QtCore import Qt, pyqtSlot + QInputDialog, QFrame, QSpinBox, QComboBox, QCheckBox, + QSplitter) +from PyQt5.QtCore import Qt, pyqtSlot, QTimer import json import os +import random class PresetControls(QWidget): """Control panel for preset management""" @@ -26,6 +28,18 @@ class PresetControls(QWidget): self.current_preset = None self.presets_directory = "presets" + # Preset group functionality + self.preset_group = [] # List of preset names in the group + self.group_enabled = False + self.group_current_index = 0 + self.group_loop_count = 1 # How many times to play each preset + self.group_current_loops = 0 # Current loop count for active preset + self.group_order = "in_order" # "in_order" or "random" + self.group_pattern_note_count = 0 # Count notes played in current pattern loop + self.group_timer = QTimer() + self.group_timer.setSingleShot(True) + self.group_timer.timeout.connect(self.advance_group_preset) + # Ensure presets directory exists os.makedirs(self.presets_directory, exist_ok=True) @@ -35,22 +49,51 @@ class PresetControls(QWidget): # Connect to armed state changes self.arpeggiator.armed_state_changed.connect(self.on_armed_state_changed) + + # Connect to playing state changes for group cycling + self.arpeggiator.playing_state_changed.connect(self.on_playing_state_changed) + self.arpeggiator.pattern_step.connect(self.on_pattern_step) + + def apply_scaling(self, scale_factor): + """Apply scaling to preset controls (placeholder for future implementation)""" + # For now, preset controls don't need special scaling + # Individual buttons already use expanding size policies from their styling + pass def setup_ui(self): """Set up the user interface""" layout = QVBoxLayout(self) - # Preset list + # Create splitter to divide preset management and preset groups + splitter = QSplitter(Qt.Horizontal) + layout.addWidget(splitter) + + # Left side: Original preset management + preset_widget = QWidget() + preset_layout = QVBoxLayout(preset_widget) + preset_group = self.create_preset_list() - layout.addWidget(preset_group) + preset_layout.addWidget(preset_group) - # Preset operations operations_group = self.create_operations() - layout.addWidget(operations_group) + preset_layout.addWidget(operations_group) - # File operations file_group = self.create_file_operations() - layout.addWidget(file_group) + preset_layout.addWidget(file_group) + + splitter.addWidget(preset_widget) + + # Right side: Preset group functionality + group_widget = QWidget() + group_layout = QVBoxLayout(group_widget) + + preset_group_section = self.create_preset_group_section() + group_layout.addWidget(preset_group_section) + + splitter.addWidget(group_widget) + + # Set equal sizes for both sides + splitter.setSizes([400, 400]) def create_preset_list(self) -> QGroupBox: """Create preset list display""" @@ -153,6 +196,124 @@ class PresetControls(QWidget): return group + def create_preset_group_section(self) -> QGroupBox: + """Create preset group functionality section""" + group = QGroupBox("Preset Groups") + layout = QVBoxLayout(group) + + # Enable/Disable group cycling + self.group_enable_checkbox = QCheckBox("Enable Group Cycling") + self.group_enable_checkbox.stateChanged.connect(self.on_group_enable_changed) + layout.addWidget(self.group_enable_checkbox) + + # Current group status + status_layout = QHBoxLayout() + status_layout.addWidget(QLabel("Status:")) + self.group_status_label = QLabel("Inactive") + self.group_status_label.setStyleSheet("color: #888888;") + status_layout.addWidget(self.group_status_label) + status_layout.addStretch() + layout.addLayout(status_layout) + + # Group preset list + layout.addWidget(QLabel("Presets in Group:")) + self.group_preset_list = QListWidget() + self.group_preset_list.setMaximumHeight(150) + self.group_preset_list.setDragDropMode(QListWidget.InternalMove) # Allow reordering + layout.addWidget(self.group_preset_list) + + # Add/Remove buttons + group_buttons_layout = QHBoxLayout() + + self.add_to_group_button = QPushButton("Add Selected →") + self.add_to_group_button.setEnabled(False) + self.add_to_group_button.clicked.connect(self.add_preset_to_group) + self.add_to_group_button.setStyleSheet("background: #3a3a3a; color: #ffffff; font-weight: bold; font-size: 12px; border: 1px solid #555555; padding: 5px 10px;") + group_buttons_layout.addWidget(self.add_to_group_button) + + self.remove_from_group_button = QPushButton("← Remove") + self.remove_from_group_button.setEnabled(False) + self.remove_from_group_button.clicked.connect(self.remove_preset_from_group) + self.remove_from_group_button.setStyleSheet("background: #5a2d2d; color: #ff9999; font-weight: bold; font-size: 12px; border: 1px solid #aa5555; padding: 5px 10px;") + group_buttons_layout.addWidget(self.remove_from_group_button) + + layout.addLayout(group_buttons_layout) + + # Clear group button + self.clear_group_button = QPushButton("Clear Group") + self.clear_group_button.clicked.connect(self.clear_preset_group) + self.clear_group_button.setStyleSheet("background: #5a2d2d; color: #ff9999; font-weight: bold; font-size: 12px; border: 1px solid #aa5555; padding: 5px 10px;") + layout.addWidget(self.clear_group_button) + + # Group settings + settings_frame = QFrame() + settings_frame.setFrameStyle(QFrame.Box) + settings_layout = QGridLayout(settings_frame) + + # Loop count + settings_layout.addWidget(QLabel("Loop Count:"), 0, 0) + self.loop_count_spinbox = QSpinBox() + self.loop_count_spinbox.setRange(1, 99) + self.loop_count_spinbox.setValue(1) + self.loop_count_spinbox.valueChanged.connect(self.on_loop_count_changed) + settings_layout.addWidget(self.loop_count_spinbox, 0, 1) + + # Preset order + settings_layout.addWidget(QLabel("Order:"), 1, 0) + self.order_combo = QComboBox() + self.order_combo.addItems(["In Order", "Random"]) + self.order_combo.currentTextChanged.connect(self.on_order_changed) + settings_layout.addWidget(self.order_combo, 1, 1) + + layout.addWidget(settings_frame) + + # Manual controls + manual_layout = QHBoxLayout() + + self.prev_preset_button = QPushButton("◀ Previous") + self.prev_preset_button.setEnabled(False) + self.prev_preset_button.clicked.connect(self.goto_previous_preset) + self.prev_preset_button.setStyleSheet("background: #3a3a3a; color: #ffffff; font-weight: bold; font-size: 12px; border: 1px solid #555555; padding: 5px 10px;") + manual_layout.addWidget(self.prev_preset_button) + + self.next_preset_button = QPushButton("Next ▶") + self.next_preset_button.setEnabled(False) + self.next_preset_button.clicked.connect(self.goto_next_preset) + self.next_preset_button.setStyleSheet("background: #3a3a3a; color: #ffffff; font-weight: bold; font-size: 12px; border: 1px solid #555555; padding: 5px 10px;") + manual_layout.addWidget(self.next_preset_button) + + # TEMPORARY DEBUG BUTTON + self.debug_advance_button = QPushButton("DEBUG: Force Advance") + self.debug_advance_button.clicked.connect(self.advance_group_preset) + self.debug_advance_button.setStyleSheet("background: #5a2d5a; color: #ffaaff; font-weight: bold; font-size: 12px; border: 1px solid #8a4a8a; padding: 5px 10px;") + manual_layout.addWidget(self.debug_advance_button) + + layout.addLayout(manual_layout) + + # Master file controls + master_frame = QFrame() + master_frame.setFrameStyle(QFrame.Box) + master_layout = QGridLayout(master_frame) + + master_layout.addWidget(QLabel("Master Files:"), 0, 0, 1, 2) + + self.save_master_button = QPushButton("Save Master...") + self.save_master_button.clicked.connect(self.save_master_file) + self.save_master_button.setStyleSheet("background: #2d5a2d; color: #ffffff; font-weight: bold; font-size: 12px; border: 1px solid #4a8a4a; padding: 5px 10px;") + master_layout.addWidget(self.save_master_button, 1, 0) + + self.load_master_button = QPushButton("Load Master...") + self.load_master_button.clicked.connect(self.load_master_file) + self.load_master_button.setStyleSheet("background: #3a3a3a; color: #ffffff; font-weight: bold; font-size: 12px; border: 1px solid #555555; padding: 5px 10px;") + master_layout.addWidget(self.load_master_button, 1, 1) + + layout.addWidget(master_frame) + + # Connect group list selection + self.group_preset_list.itemSelectionChanged.connect(self.on_group_selection_changed) + + return group + def capture_current_settings(self) -> dict: """Capture current settings into a preset dictionary""" preset = { @@ -163,6 +324,7 @@ class PresetControls(QWidget): "arpeggiator": { "root_note": self.arpeggiator.root_note, "scale": self.arpeggiator.scale, + "scale_note_start": self.arpeggiator.scale_note_start, "pattern_type": self.arpeggiator.pattern_type, "octave_range": self.arpeggiator.octave_range, "note_speed": self.arpeggiator.note_speed, @@ -170,7 +332,7 @@ class PresetControls(QWidget): "swing": self.arpeggiator.swing, "velocity": self.arpeggiator.velocity, "tempo": self.arpeggiator.tempo, - "pattern_length": getattr(self.arpeggiator, 'pattern_length', 8), + "user_pattern_length": getattr(self.arpeggiator, 'user_pattern_length', 8), "channel_distribution": self.arpeggiator.channel_distribution, "delay_enabled": self.arpeggiator.delay_enabled, "delay_length": self.arpeggiator.delay_length, @@ -211,6 +373,7 @@ class PresetControls(QWidget): arp_settings = preset.get("arpeggiator", {}) self.arpeggiator.set_root_note(arp_settings.get("root_note", 60)) self.arpeggiator.set_scale(arp_settings.get("scale", "major")) + self.arpeggiator.set_scale_note_start(arp_settings.get("scale_note_start", 0)) self.arpeggiator.set_pattern_type(arp_settings.get("pattern_type", "up")) self.arpeggiator.set_octave_range(arp_settings.get("octave_range", 1)) self.arpeggiator.set_note_speed(arp_settings.get("note_speed", "1/8")) @@ -219,10 +382,12 @@ class PresetControls(QWidget): self.arpeggiator.set_velocity(arp_settings.get("velocity", 80)) self.arpeggiator.set_tempo(arp_settings.get("tempo", 120.0)) - # Apply pattern length if available - if "pattern_length" in arp_settings: - if hasattr(self.arpeggiator, 'set_pattern_length'): - self.arpeggiator.set_pattern_length(arp_settings["pattern_length"]) + # Apply user pattern length (check both old and new names for compatibility) + pattern_length = arp_settings.get("user_pattern_length") or arp_settings.get("pattern_length", 8) + if hasattr(self.arpeggiator, 'set_user_pattern_length'): + self.arpeggiator.set_user_pattern_length(pattern_length) + elif hasattr(self.arpeggiator, 'set_pattern_length'): + self.arpeggiator.set_pattern_length(pattern_length) # Apply channel distribution self.arpeggiator.set_channel_distribution(arp_settings.get("channel_distribution", "up")) @@ -299,6 +464,9 @@ class PresetControls(QWidget): self.rename_button.setEnabled(has_selection) self.duplicate_button.setEnabled(has_selection) self.export_button.setEnabled(has_selection) + + # Update group UI state (for add button enablement) + self.update_group_ui_state() except RuntimeError: # Item was deleted, disable all buttons self.load_button.setEnabled(False) @@ -629,4 +797,435 @@ class PresetControls(QWidget): def on_armed_state_changed(self): """Handle armed state changes""" # Update UI colors when armed state changes - self.update_preset_list_colors() \ No newline at end of file + self.update_preset_list_colors() + + # ======= PRESET GROUP FUNCTIONALITY ======= + + def on_group_enable_changed(self, state): + """Handle group cycling enable/disable""" + self.group_enabled = state == Qt.Checked + print(f"DEBUG: Group cycling enabled changed to: {self.group_enabled} (state={state})") + + if self.group_enabled and len(self.preset_group) > 0: + print("DEBUG: Calling start_group_cycling") + self.start_group_cycling() + else: + print("DEBUG: Calling stop_group_cycling") + self.stop_group_cycling() + + self.update_group_ui_state() + + def on_group_selection_changed(self): + """Handle selection change in group preset list""" + self.remove_from_group_button.setEnabled(len(self.group_preset_list.selectedItems()) > 0) + + def on_loop_count_changed(self, value): + """Handle loop count change""" + print(f"DEBUG: Loop count changed from {self.group_loop_count} to {value}") + self.group_loop_count = value + # Only reset current loops if we're not actively cycling + if not self.group_enabled or not self.group_timer.isActive(): + print("DEBUG: Resetting current loops (not actively cycling)") + self.group_current_loops = 0 + else: + print("DEBUG: NOT resetting current loops (actively cycling)") + + def on_order_changed(self, text): + """Handle order change""" + self.group_order = "random" if text == "Random" else "in_order" + + # If random, shuffle the current group + if self.group_order == "random" and len(self.preset_group) > 1: + # Create a new random order without changing the original list + pass # We'll handle randomization in advance_group_preset + + def add_preset_to_group(self): + """Add selected preset to the group""" + current_item = self.preset_list.currentItem() + if current_item: + preset_name = current_item.text() + if preset_name not in self.preset_group: + self.preset_group.append(preset_name) + self.update_group_preset_list() + self.update_group_ui_state() + + def remove_preset_from_group(self): + """Remove selected preset from the group""" + current_item = self.group_preset_list.currentItem() + if current_item: + preset_name = current_item.text() + if preset_name in self.preset_group: + self.preset_group.remove(preset_name) + self.update_group_preset_list() + self.update_group_ui_state() + + def clear_preset_group(self): + """Clear all presets from the group""" + self.preset_group.clear() + self.stop_group_cycling() + self.update_group_preset_list() + self.update_group_ui_state() + + def update_group_preset_list(self): + """Update the group preset list display""" + self.group_preset_list.clear() + for preset_name in self.preset_group: + item = QListWidgetItem(preset_name) + # Highlight current preset in group + if self.group_enabled and preset_name == self.get_current_group_preset(): + item.setBackground(Qt.darkBlue) + self.group_preset_list.addItem(item) + + def update_group_ui_state(self): + """Update group UI elements based on current state""" + has_presets = len(self.preset_group) > 0 + is_active = self.group_enabled and has_presets + + # Update status + if is_active: + current_preset = self.get_current_group_preset() + # Show note progress instead of loop progress + pattern_length = self.arpeggiator.user_pattern_length if hasattr(self.arpeggiator, 'user_pattern_length') else 0 + total_notes_needed = pattern_length * self.group_loop_count + progress_text = f" (Note {self.group_pattern_note_count}/{total_notes_needed})" + self.group_status_label.setText(f"Active: {current_preset}{progress_text}") + self.group_status_label.setStyleSheet("color: #00aa00; font-weight: bold;") + elif self.group_enabled: + self.group_status_label.setText("Enabled - No Presets") + self.group_status_label.setStyleSheet("color: #aaaa00;") + else: + self.group_status_label.setText("Inactive") + self.group_status_label.setStyleSheet("color: #888888;") + + # Update button states + self.prev_preset_button.setEnabled(is_active) + self.next_preset_button.setEnabled(is_active) + + # Update add button based on selection + current_item = self.preset_list.currentItem() + can_add = (current_item is not None and + current_item.text() not in self.preset_group) + self.add_to_group_button.setEnabled(can_add) + + def start_group_cycling(self): + """Start automatic group cycling""" + print(f"DEBUG: start_group_cycling called with {len(self.preset_group)} presets in group") + + if len(self.preset_group) > 0: + # Only reset position if we're not already cycling + if not self.group_timer.isActive(): + print("DEBUG: Resetting group position (first time start)") + self.group_current_index = 0 + self.group_current_loops = 0 + + # Load first preset + first_preset = self.preset_group[0] + print(f"DEBUG: Loading first preset: '{first_preset}'") + + if first_preset in self.presets: + self.apply_preset_settings(self.presets[first_preset]) + self.current_preset = first_preset + print(f"DEBUG: Successfully loaded first preset: '{first_preset}'") + else: + print(f"DEBUG: ERROR - First preset '{first_preset}' not found in presets!") + else: + print("DEBUG: Group cycling already active, not resetting position") + + # Initialize pattern note counter + self.group_pattern_note_count = 0 + + self.update_group_preset_list() + print("DEBUG: Group cycling started - waiting for arpeggiator to start playing") + + def stop_group_cycling(self): + """Stop automatic group cycling""" + self.group_timer.stop() + + # Disconnect from arpeggiator if connected + if hasattr(self.arpeggiator, 'pattern_completed'): + try: + self.arpeggiator.pattern_completed.disconnect(self.on_pattern_completed) + except TypeError: + pass # Already disconnected + + def get_current_group_preset(self): + """Get the currently active preset in the group""" + if 0 <= self.group_current_index < len(self.preset_group): + return self.preset_group[self.group_current_index] + return None + + def advance_group_preset(self): + """Advance to the next preset in the group""" + print(f"DEBUG: advance_group_preset called - enabled: {self.group_enabled}, group_size: {len(self.preset_group)}") + + if not self.group_enabled or len(self.preset_group) == 0: + print("DEBUG: advance_group_preset - early return (not enabled or no presets)") + return + + old_index = self.group_current_index + + # Move to next preset + if self.group_order == "random": + print("DEBUG: Using random order") + # Pick a random preset that's different from current (if possible) + if len(self.preset_group) > 1: + available_indices = [i for i in range(len(self.preset_group)) + if i != self.group_current_index] + self.group_current_index = random.choice(available_indices) + # If only one preset, stay on it + else: # in_order + print("DEBUG: Using in_order") + self.group_current_index = (self.group_current_index + 1) % len(self.preset_group) + + print(f"DEBUG: Index changed from {old_index} to {self.group_current_index}") + + # Load the next preset + next_preset = self.preset_group[self.group_current_index] + print(f"DEBUG: Loading next preset: '{next_preset}'") + + if next_preset in self.presets: + self.apply_preset_settings(self.presets[next_preset]) + self.current_preset = next_preset + print(f"Group cycling: Advanced to preset '{next_preset}' (index {self.group_current_index})") + else: + print(f"DEBUG: ERROR - preset '{next_preset}' not found in presets dict!") + + self.update_group_ui_state() + self.update_group_preset_list() + + def goto_previous_preset(self): + """Manually go to previous preset in group""" + if not self.group_enabled or len(self.preset_group) <= 1: + return + + self.group_current_index = (self.group_current_index - 1) % len(self.preset_group) + self.group_current_loops = 0 + + prev_preset = self.preset_group[self.group_current_index] + if prev_preset in self.presets: + self.apply_preset_settings(self.presets[prev_preset]) + self.current_preset = prev_preset + + self.update_group_ui_state() + self.update_group_preset_list() + + def goto_next_preset(self): + """Manually go to next preset in group""" + if not self.group_enabled or len(self.preset_group) <= 1: + return + + if self.group_order == "random": + # Pick a random preset that's different from current (if possible) + if len(self.preset_group) > 1: + available_indices = [i for i in range(len(self.preset_group)) + if i != self.group_current_index] + self.group_current_index = random.choice(available_indices) + else: # in_order + self.group_current_index = (self.group_current_index + 1) % len(self.preset_group) + + self.group_current_loops = 0 + + next_preset = self.preset_group[self.group_current_index] + if next_preset in self.presets: + self.apply_preset_settings(self.presets[next_preset]) + self.current_preset = next_preset + + self.update_group_ui_state() + self.update_group_preset_list() + + # Note: Timer-based cycling replaced with note-counting approach + + def on_pattern_completed(self): + """Handle arpeggiator pattern completion for timing""" + # This method exists for future integration with arpeggiator pattern signals + # For now, we use the playing state change to initiate cycling + pass + + def on_playing_state_changed(self, is_playing): + """Handle arpeggiator play/stop state changes""" + print(f"DEBUG: on_playing_state_changed called - is_playing: {is_playing}, group_enabled: {self.group_enabled}, group_size: {len(self.preset_group)}") + + if is_playing and self.group_enabled and len(self.preset_group) > 0: + print("DEBUG: Arpeggiator started - resetting note counter") + # Reset note counter when arpeggiator starts playing + self.group_pattern_note_count = 0 + elif not is_playing: + print("DEBUG: Arpeggiator stopped") + # Stop any pending preset changes when arpeggiator stops + self.group_timer.stop() + + def on_pattern_step(self, current_step): + """Handle each pattern step (note) played by the arpeggiator""" + if not self.group_enabled or len(self.preset_group) == 0: + return + + # Increment our note counter + self.group_pattern_note_count += 1 + + # Calculate how many notes should be played for current preset + pattern_length = self.arpeggiator.user_pattern_length + total_notes_needed = pattern_length * self.group_loop_count + + print(f"DEBUG: Pattern step {current_step}, note count: {self.group_pattern_note_count}/{total_notes_needed}") + + # Check if we've played enough notes to advance to next preset + if self.group_pattern_note_count >= total_notes_needed: + print("DEBUG: Note count reached, advancing to next preset") + self.group_pattern_note_count = 0 # Reset counter + self.advance_group_preset() + + # ======= MASTER FILE FUNCTIONALITY ======= + + def save_master_file(self): + """Save current presets and group configuration as a master file""" + try: + # Create master files directory if it doesn't exist + master_dir = "master_files" + os.makedirs(master_dir, exist_ok=True) + + # Open file dialog + filename, _ = QFileDialog.getSaveFileName( + self, + "Save Master File", + os.path.join(master_dir, "master.json"), + "Master Files (*.json);;All Files (*)" + ) + + if not filename: + return + + # Capture master file data + master_data = { + "version": "1.0", + "timestamp": os.path.basename(filename).replace('.json', ''), + "type": "master_file", + + # All individual presets + "presets": self.presets.copy(), + + # Preset group configuration + "preset_group": { + "enabled": self.group_enabled, + "presets": self.preset_group.copy(), + "loop_count": self.group_loop_count, + "order": self.group_order, + "current_index": self.group_current_index, + "current_loops": self.group_current_loops + } + } + + # Add timestamp + from datetime import datetime + master_data["timestamp"] = datetime.now().isoformat() + + # Write to file + with open(filename, 'w') as f: + json.dump(master_data, f, indent=2) + + QMessageBox.information(self, "Master File Saved", + f"Master file saved successfully:\n{filename}") + + except Exception as e: + QMessageBox.critical(self, "Error", f"Failed to save master file:\n{str(e)}") + + def load_master_file(self): + """Load presets and group configuration from a master file""" + try: + # Open file dialog + master_dir = "master_files" + os.makedirs(master_dir, exist_ok=True) + + filename, _ = QFileDialog.getOpenFileName( + self, + "Load Master File", + master_dir, + "Master Files (*.json);;All Files (*)" + ) + + if not filename: + return + + # Confirm loading (this will replace current presets) + reply = QMessageBox.question( + self, + "Load Master File", + "This will replace all current presets and group configuration.\n" + "Are you sure you want to continue?", + QMessageBox.Yes | QMessageBox.No, + QMessageBox.No + ) + + if reply != QMessageBox.Yes: + return + + # Load master file + with open(filename, 'r') as f: + master_data = json.load(f) + + # Validate master file + if master_data.get("type") != "master_file": + QMessageBox.warning(self, "Invalid File", + "This doesn't appear to be a valid master file.") + return + + # Stop any active group cycling + self.stop_group_cycling() + + # Load presets + loaded_presets = master_data.get("presets", {}) + self.presets = loaded_presets.copy() + + # Update preset list display + self.update_preset_list() + + # Load group configuration + group_config = master_data.get("preset_group", {}) + self.preset_group = group_config.get("presets", []) + self.group_loop_count = group_config.get("loop_count", 1) + self.group_order = group_config.get("order", "in_order") + self.group_current_index = 0 # Reset to start + self.group_current_loops = 0 # Reset loops + + # Update UI controls + self.loop_count_spinbox.setValue(self.group_loop_count) + order_text = "Random" if self.group_order == "random" else "In Order" + self.order_combo.setCurrentText(order_text) + + # Update group list display + self.update_group_preset_list() + + # Don't auto-enable group cycling - let user decide + self.group_enabled = False + self.group_enable_checkbox.setChecked(False) + + # Update all UI states + self.update_group_ui_state() + self.update_preset_list_colors() + + loaded_count = len(loaded_presets) + group_count = len(self.preset_group) + + QMessageBox.information( + self, + "Master File Loaded", + f"Successfully loaded:\n" + f"• {loaded_count} presets\n" + f"• Preset group with {group_count} presets\n\n" + f"From: {os.path.basename(filename)}" + ) + + except Exception as e: + QMessageBox.critical(self, "Error", f"Failed to load master file:\n{str(e)}") + + def update_preset_list(self): + """Update the main preset list display""" + self.preset_list.clear() + for preset_name in sorted(self.presets.keys()): + item = QListWidgetItem(preset_name) + self.preset_list.addItem(item) + + # Update current preset display + if self.current_preset and self.current_preset in self.presets: + self.current_preset_label.setText(self.current_preset) + else: + self.current_preset_label.setText("None") \ No newline at end of file diff --git a/gui/volume_controls.py b/gui/volume_controls.py index aad743d..c11fc19 100644 --- a/gui/volume_controls.py +++ b/gui/volume_controls.py @@ -6,7 +6,7 @@ Interface for tempo-linked volume and brightness pattern controls. from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QGridLayout, QGroupBox, QComboBox, QSlider, QSpinBox, QLabel, - QPushButton, QFrame, QScrollArea) + QPushButton, QFrame, QScrollArea, QSizePolicy) from PyQt5.QtCore import Qt, pyqtSlot class VolumeControls(QWidget): @@ -38,9 +38,89 @@ class VolumeControls(QWidget): self.current_pattern = "static" self.armed_pattern_button = None self.pattern_buttons = {} + + # Scaling support + self.scale_factor = 1.0 + self.setup_ui() self.connect_signals() + def apply_scaling(self, scale_factor): + """Apply new scaling factor to all buttons""" + self.scale_factor = scale_factor + + # Update all pattern buttons with new scaling + for button in self.pattern_buttons.values(): + self.update_pattern_button_style_with_scale(button, self.get_button_state(button)) + + def get_button_state(self, button): + """Determine button state from current styling""" + style = button.styleSheet() + if "#2d5a2d" in style or "#00aa44" in style: + return "active" + elif "#ff8800" in style: + return "armed" + else: + return "inactive" + + def update_pattern_button_style_with_scale(self, button, state): + """Update pattern button styling with current scale factor""" + font_size = max(8, int(12 * self.scale_factor)) + padding = max(2, int(5 * self.scale_factor)) + min_height = max(20, int(30 * self.scale_factor)) + + # Set size policy for expansion + button.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + + if state == "active": + button.setStyleSheet(f""" + QPushButton {{ + background: #2d5a2d; + color: white; + border: 1px solid #4a8a4a; + font-weight: bold; + font-size: {font_size}px; + min-height: {min_height}px; + padding: {padding}px; + }} + QPushButton:hover {{ + background: #3d6a3d; + border: 1px solid #5aaa5a; + }} + """) + elif state == "armed": + button.setStyleSheet(f""" + QPushButton {{ + background: #ff8800; + color: white; + border: 1px solid #ffaa00; + font-weight: bold; + font-size: {font_size}px; + min-height: {min_height}px; + padding: {padding}px; + }} + QPushButton:hover {{ + background: #ffaa00; + border: 1px solid #ffcc33; + }} + """) + else: # inactive + button.setStyleSheet(f""" + QPushButton {{ + background: #3a3a3a; + color: #ffffff; + border: 1px solid #555555; + font-weight: bold; + font-size: {font_size}px; + min-height: {min_height}px; + padding: {padding}px; + }} + QPushButton:hover {{ + background: #505050; + border: 1px solid #777777; + }} + """) + def setup_ui(self): """Set up the user interface""" layout = QVBoxLayout(self) @@ -105,42 +185,7 @@ class VolumeControls(QWidget): def update_pattern_button_style(self, button, state): """Update pattern button styling based on state""" - if state == "active": - button.setStyleSheet(""" - QPushButton { - background: #2d5a2d; - color: white; - border: 1px solid #4a8a4a; - font-weight: bold; - font-size: 12px; - min-height: 30px; - padding: 5px 10px; - } - """) - elif state == "armed": - button.setStyleSheet(""" - QPushButton { - background: #ff8800; - color: white; - border: 1px solid #ffaa00; - font-weight: bold; - font-size: 12px; - min-height: 30px; - padding: 5px 10px; - } - """) - else: # inactive - button.setStyleSheet(""" - QPushButton { - background: #3a3a3a; - color: #ffffff; - border: 1px solid #555555; - font-weight: bold; - font-size: 12px; - min-height: 30px; - padding: 5px 10px; - } - """) + self.update_pattern_button_style_with_scale(button, state) def create_global_settings(self) -> QGroupBox: """Create global volume/velocity range settings""" diff --git a/master_files/example_master.json b/master_files/example_master.json new file mode 100644 index 0000000..a1c47c2 --- /dev/null +++ b/master_files/example_master.json @@ -0,0 +1,129 @@ +{ + "version": "1.0", + "timestamp": "2025-09-09T18:57:00.000000", + "type": "master_file", + "presets": { + "Slow Ambient": { + "version": "1.0", + "timestamp": "2025-09-09T18:57:00.000000", + "arpeggiator": { + "root_note": 60, + "scale": "minor", + "scale_note_start": 0, + "pattern_type": "up", + "octave_range": 2, + "note_speed": "1/2", + "gate": 0.8, + "swing": 0.0, + "velocity": 60, + "tempo": 70.0, + "user_pattern_length": 8, + "channel_distribution": "up", + "delay_enabled": true, + "delay_length": 3, + "delay_timing": "1/4", + "delay_fade": 0.5 + }, + "channels": { + "active_synth_count": 4, + "channel_instruments": { + "1": 0, "2": 0, "3": 0, "4": 0, "5": 0, "6": 0, "7": 0, "8": 0, + "9": 0, "10": 0, "11": 0, "12": 0, "13": 0, "14": 0, "15": 0, "16": 0 + } + }, + "volume_patterns": { + "current_pattern": "swell", + "pattern_speed": 0.5, + "pattern_intensity": 0.8, + "global_volume_range": [0.2, 0.7], + "global_velocity_range": [40, 80], + "channel_volume_ranges": {}, + "velocity_ranges": {} + } + }, + "Fast Dance": { + "version": "1.0", + "timestamp": "2025-09-09T18:57:00.000000", + "arpeggiator": { + "root_note": 64, + "scale": "major", + "scale_note_start": 0, + "pattern_type": "up_down", + "octave_range": 1, + "note_speed": "1/16", + "gate": 0.5, + "swing": 0.2, + "velocity": 120, + "tempo": 140.0, + "user_pattern_length": 4, + "channel_distribution": "bounce", + "delay_enabled": false, + "delay_length": 0, + "delay_timing": "1/8", + "delay_fade": 0.3 + }, + "channels": { + "active_synth_count": 8, + "channel_instruments": { + "1": 0, "2": 0, "3": 0, "4": 0, "5": 0, "6": 0, "7": 0, "8": 0, + "9": 0, "10": 0, "11": 0, "12": 0, "13": 0, "14": 0, "15": 0, "16": 0 + } + }, + "volume_patterns": { + "current_pattern": "random_sparkle", + "pattern_speed": 2.0, + "pattern_intensity": 1.0, + "global_volume_range": [0.6, 1.0], + "global_velocity_range": [100, 127], + "channel_volume_ranges": {}, + "velocity_ranges": {} + } + }, + "Med Groove": { + "version": "1.0", + "timestamp": "2025-09-09T18:57:00.000000", + "arpeggiator": { + "root_note": 67, + "scale": "dorian", + "scale_note_start": 2, + "pattern_type": "down_up", + "octave_range": 2, + "note_speed": "1/8", + "gate": 0.7, + "swing": 0.1, + "velocity": 90, + "tempo": 110.0, + "user_pattern_length": 6, + "channel_distribution": "single_channel", + "delay_enabled": true, + "delay_length": 2, + "delay_timing": "1/8T", + "delay_fade": 0.4 + }, + "channels": { + "active_synth_count": 3, + "channel_instruments": { + "1": 0, "2": 0, "3": 0, "4": 0, "5": 0, "6": 0, "7": 0, "8": 0, + "9": 0, "10": 0, "11": 0, "12": 0, "13": 0, "14": 0, "15": 0, "16": 0 + } + }, + "volume_patterns": { + "current_pattern": "accent_4", + "pattern_speed": 1.0, + "pattern_intensity": 0.9, + "global_volume_range": [0.3, 0.9], + "global_velocity_range": [70, 110], + "channel_volume_ranges": {}, + "velocity_ranges": {} + } + } + }, + "preset_group": { + "enabled": false, + "presets": ["Slow Ambient", "Med Groove", "Fast Dance"], + "loop_count": 2, + "order": "in_order", + "current_index": 0, + "current_loops": 0 + } +} \ No newline at end of file diff --git a/master_files/test master.json b/master_files/test master.json new file mode 100644 index 0000000..31628e7 --- /dev/null +++ b/master_files/test master.json @@ -0,0 +1,193 @@ +{ + "version": "1.0", + "timestamp": "2025-09-09T13:59:32.284079", + "type": "master_file", + "presets": { + "two Copy Copy": { + "version": "1.0", + "timestamp": "2025-09-09T13:51:05.409379", + "arpeggiator": { + "root_note": 60, + "scale": "mixolydian", + "scale_note_start": 3, + "pattern_type": "down", + "octave_range": 1, + "note_speed": "1/2", + "gate": 0.71, + "swing": 0.0, + "velocity": 127, + "tempo": 120.0, + "user_pattern_length": 3, + "channel_distribution": "up", + "delay_enabled": true, + "delay_length": 2, + "delay_timing": "2/1T", + "delay_fade": 0.44 + }, + "channels": { + "active_synth_count": 3, + "channel_instruments": { + "1": 0, + "2": 0, + "3": 0, + "4": 0, + "5": 0, + "6": 0, + "7": 0, + "8": 0, + "9": 0, + "10": 0, + "11": 0, + "12": 0, + "13": 0, + "14": 0, + "15": 0, + "16": 0 + } + }, + "volume_patterns": { + "current_pattern": "static", + "pattern_speed": 2.0, + "pattern_intensity": 1.0, + "global_volume_range": [ + 0.0, + 1.0 + ], + "global_velocity_range": [ + 40, + 127 + ], + "channel_volume_ranges": {}, + "velocity_ranges": {} + } + }, + "two Copy": { + "version": "1.0", + "timestamp": "2025-09-09T13:50:13.070241", + "arpeggiator": { + "root_note": 60, + "scale": "mixolydian", + "scale_note_start": 0, + "pattern_type": "down", + "octave_range": 1, + "note_speed": "1/2", + "gate": 0.71, + "swing": 0.0, + "velocity": 127, + "tempo": 120.0, + "user_pattern_length": 3, + "channel_distribution": "up", + "delay_enabled": true, + "delay_length": 2, + "delay_timing": "2/1T", + "delay_fade": 0.44 + }, + "channels": { + "active_synth_count": 3, + "channel_instruments": { + "1": 0, + "2": 0, + "3": 0, + "4": 0, + "5": 0, + "6": 0, + "7": 0, + "8": 0, + "9": 0, + "10": 0, + "11": 0, + "12": 0, + "13": 0, + "14": 0, + "15": 0, + "16": 0 + } + }, + "volume_patterns": { + "current_pattern": "static", + "pattern_speed": 2.0, + "pattern_intensity": 1.0, + "global_volume_range": [ + 0.0, + 1.0 + ], + "global_velocity_range": [ + 40, + 127 + ], + "channel_volume_ranges": {}, + "velocity_ranges": {} + } + }, + "two": { + "version": "1.0", + "timestamp": "2025-09-09T13:49:16.446087", + "arpeggiator": { + "root_note": 60, + "scale": "mixolydian", + "scale_note_start": 1, + "pattern_type": "down", + "octave_range": 1, + "note_speed": "1/2", + "gate": 0.71, + "swing": 0.0, + "velocity": 127, + "tempo": 120.0, + "user_pattern_length": 3, + "channel_distribution": "up", + "delay_enabled": true, + "delay_length": 2, + "delay_timing": "2/1T", + "delay_fade": 0.44 + }, + "channels": { + "active_synth_count": 3, + "channel_instruments": { + "1": 0, + "2": 0, + "3": 0, + "4": 0, + "5": 0, + "6": 0, + "7": 0, + "8": 0, + "9": 0, + "10": 0, + "11": 0, + "12": 0, + "13": 0, + "14": 0, + "15": 0, + "16": 0 + } + }, + "volume_patterns": { + "current_pattern": "static", + "pattern_speed": 2.0, + "pattern_intensity": 1.0, + "global_volume_range": [ + 0.0, + 1.0 + ], + "global_velocity_range": [ + 40, + 127 + ], + "channel_volume_ranges": {}, + "velocity_ranges": {} + } + } + }, + "preset_group": { + "enabled": true, + "presets": [ + "two", + "two Copy", + "two Copy Copy" + ], + "loop_count": 1, + "order": "in_order", + "current_index": 0, + "current_loops": 0 + } +} \ No newline at end of file diff --git a/presets/butt 2.json b/presets/two Copy Copy.json similarity index 72% rename from presets/butt 2.json rename to presets/two Copy Copy.json index 61b8c57..d873174 100644 --- a/presets/butt 2.json +++ b/presets/two Copy Copy.json @@ -1,22 +1,23 @@ { "version": "1.0", - "timestamp": "2025-09-09T08:50:29.583440", + "timestamp": "2025-09-09T13:51:05.409379", "arpeggiator": { - "root_note": 62, - "scale": "major", + "root_note": 60, + "scale": "mixolydian", + "scale_note_start": 3, "pattern_type": "down", "octave_range": 1, - "note_speed": "1/4", + "note_speed": "1/2", "gate": 0.71, "swing": 0.0, - "velocity": 47, + "velocity": 127, "tempo": 120.0, - "pattern_length": 3, + "user_pattern_length": 3, "channel_distribution": "up", - "delay_enabled": false, - "delay_length": 3, + "delay_enabled": true, + "delay_length": 2, "delay_timing": "2/1T", - "delay_fade": 0.9 + "delay_fade": 0.44 }, "channels": { "active_synth_count": 3, @@ -40,7 +41,7 @@ } }, "volume_patterns": { - "current_pattern": "accent_4", + "current_pattern": "static", "pattern_speed": 2.0, "pattern_intensity": 1.0, "global_volume_range": [ diff --git a/presets/two Copy.json b/presets/two Copy.json new file mode 100644 index 0000000..fa0bb1b --- /dev/null +++ b/presets/two Copy.json @@ -0,0 +1,58 @@ +{ + "version": "1.0", + "timestamp": "2025-09-09T13:50:13.070241", + "arpeggiator": { + "root_note": 60, + "scale": "mixolydian", + "scale_note_start": 0, + "pattern_type": "down", + "octave_range": 1, + "note_speed": "1/2", + "gate": 0.71, + "swing": 0.0, + "velocity": 127, + "tempo": 120.0, + "user_pattern_length": 3, + "channel_distribution": "up", + "delay_enabled": true, + "delay_length": 2, + "delay_timing": "2/1T", + "delay_fade": 0.44 + }, + "channels": { + "active_synth_count": 3, + "channel_instruments": { + "1": 0, + "2": 0, + "3": 0, + "4": 0, + "5": 0, + "6": 0, + "7": 0, + "8": 0, + "9": 0, + "10": 0, + "11": 0, + "12": 0, + "13": 0, + "14": 0, + "15": 0, + "16": 0 + } + }, + "volume_patterns": { + "current_pattern": "static", + "pattern_speed": 2.0, + "pattern_intensity": 1.0, + "global_volume_range": [ + 0.0, + 1.0 + ], + "global_velocity_range": [ + 40, + 127 + ], + "channel_volume_ranges": {}, + "velocity_ranges": {} + } +} \ No newline at end of file diff --git a/presets/two.json b/presets/two.json new file mode 100644 index 0000000..9aebdad --- /dev/null +++ b/presets/two.json @@ -0,0 +1,58 @@ +{ + "version": "1.0", + "timestamp": "2025-09-09T13:49:16.446087", + "arpeggiator": { + "root_note": 60, + "scale": "mixolydian", + "scale_note_start": 1, + "pattern_type": "down", + "octave_range": 1, + "note_speed": "1/2", + "gate": 0.71, + "swing": 0.0, + "velocity": 127, + "tempo": 120.0, + "user_pattern_length": 3, + "channel_distribution": "up", + "delay_enabled": true, + "delay_length": 2, + "delay_timing": "2/1T", + "delay_fade": 0.44 + }, + "channels": { + "active_synth_count": 3, + "channel_instruments": { + "1": 0, + "2": 0, + "3": 0, + "4": 0, + "5": 0, + "6": 0, + "7": 0, + "8": 0, + "9": 0, + "10": 0, + "11": 0, + "12": 0, + "13": 0, + "14": 0, + "15": 0, + "16": 0 + } + }, + "volume_patterns": { + "current_pattern": "static", + "pattern_speed": 2.0, + "pattern_intensity": 1.0, + "global_volume_range": [ + 0.0, + 1.0 + ], + "global_velocity_range": [ + 40, + 127 + ], + "channel_volume_ranges": {}, + "velocity_ranges": {} + } +} \ No newline at end of file