aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--cli-project/pyproject.toml4
-rw-r--r--cli-project/src/solo_tool_cli.py15
-rw-r--r--doc/diagram.drawio429
-rw-r--r--readme.md2
-rw-r--r--solo-tool-project/pyproject.toml4
-rw-r--r--solo-tool-project/src/solo_tool/handlers.py42
-rw-r--r--solo-tool-project/src/solo_tool/midi_controller_launchpad_mini.py44
-rw-r--r--solo-tool-project/src/solo_tool/solo_tool.py6
-rw-r--r--solo-tool-project/test/midi_launchpad_mini_integrationtest.py8
-rw-r--r--solo-tool-project/test/solo_tool_integrationtest.py8
-rw-r--r--web-project/pyproject.toml4
-rw-r--r--web-project/src/solo_tool_web.py99
12 files changed, 147 insertions, 518 deletions
diff --git a/cli-project/pyproject.toml b/cli-project/pyproject.toml
index 3e2c855..489d1ec 100644
--- a/cli-project/pyproject.toml
+++ b/cli-project/pyproject.toml
@@ -8,9 +8,9 @@ authors = [
{ name = "Eddy Pedroni", email = "epedroni@pm.me" },
]
description = "A CLI frontend for the solo_tool library"
-requires-python = ">=3.12"
+requires-python = ">=3.13"
dependencies = [
- "solo_tool"
+ "solo_tool>=2.0"
]
dynamic = ["version"]
diff --git a/cli-project/src/solo_tool_cli.py b/cli-project/src/solo_tool_cli.py
index 5cc1537..d0f39c1 100644
--- a/cli-project/src/solo_tool_cli.py
+++ b/cli-project/src/solo_tool_cli.py
@@ -3,6 +3,7 @@ import time
from solo_tool import SoloTool
from solo_tool.midi_controller_launchpad_mini import MidiController
+from solo_tool.session_manager import loadSession
def main():
args = sys.argv[1:]
@@ -10,20 +11,16 @@ def main():
print("Please provide path to session file")
sys.exit(1)
- soloTool = SoloTool()
- soloTool.loadSession(args[0])
-
- def tick():
- soloTool.tick()
- threading.Timer(0.1, tick).start()
+ soloTool = loadSession(args[0])
midiController = MidiController(soloTool)
midiController.connect()
try:
- while(True):
- time.sleep(0.1)
- soloTool.tick()
+ while True:
+ raw = input("> ")
+ if raw == "q":
+ break
except KeyboardInterrupt:
pass
finally:
diff --git a/doc/diagram.drawio b/doc/diagram.drawio
index ee5ea5e..bb8dbd0 100644
--- a/doc/diagram.drawio
+++ b/doc/diagram.drawio
@@ -1,375 +1,6 @@
-<mxfile host="Electron" modified="2025-02-22T17:51:11.071Z" agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/22.1.2 Chrome/114.0.5735.289 Electron/25.9.4 Safari/537.36" etag="jME-aKyU6IcygTFz2vXw" version="22.1.2" type="device" pages="4">
- <diagram id="g-wcGVps3MkI6_XAwNEs" name="Core">
- <mxGraphModel dx="1562" dy="963" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1169" pageHeight="827" math="0" shadow="0">
- <root>
- <mxCell id="0" />
- <mxCell id="1" parent="0" />
- <mxCell id="5IB1TeDA8rQgVov2ilYq-1" value="solo tool" style="rounded=0;whiteSpace=wrap;html=1;dashed=1;glass=0;shadow=0;sketch=0;fillColor=none;align=left;verticalAlign=top;" parent="1" vertex="1">
- <mxGeometry x="410" y="227" width="530" height="693" as="geometry" />
- </mxCell>
- <mxCell id="MU1YSbBTE73kW5gn9q-V-5" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=1;entryY=0.75;entryDx=0;entryDy=0;" parent="1" source="718ck8ZuCs3BOJF-nClt-3" target="MU1YSbBTE73kW5gn9q-V-3" edge="1">
- <mxGeometry relative="1" as="geometry" />
- </mxCell>
- <mxCell id="MU1YSbBTE73kW5gn9q-V-6" value="&lt;div&gt;Current&lt;/div&gt;&lt;div&gt;position&lt;br&gt;&lt;/div&gt;" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="MU1YSbBTE73kW5gn9q-V-5" vertex="1" connectable="0">
- <mxGeometry x="0.2328" y="-1" relative="1" as="geometry">
- <mxPoint x="10" y="1" as="offset" />
- </mxGeometry>
- </mxCell>
- <mxCell id="9fq4LfI0W2HX4gTsbRt6-1" value="&lt;div&gt;media player&lt;/div&gt;" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" parent="1" vertex="1">
- <mxGeometry x="440" y="648" width="160" height="80" as="geometry" />
- </mxCell>
- <mxCell id="9fq4LfI0W2HX4gTsbRt6-3" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;exitX=0.5;exitY=1;exitDx=0;exitDy=0;" parent="1" source="MU1YSbBTE73kW5gn9q-V-11" target="9fq4LfI0W2HX4gTsbRt6-1" edge="1">
- <mxGeometry relative="1" as="geometry">
- <mxPoint x="300" y="508.0344827586207" as="sourcePoint" />
- </mxGeometry>
- </mxCell>
- <mxCell id="MU1YSbBTE73kW5gn9q-V-12" value="&lt;div&gt;Set current&lt;/div&gt;&lt;div&gt;song&lt;br&gt;&lt;/div&gt;" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="9fq4LfI0W2HX4gTsbRt6-3" vertex="1" connectable="0">
- <mxGeometry x="0.2112" relative="1" as="geometry">
- <mxPoint y="-47" as="offset" />
- </mxGeometry>
- </mxCell>
- <mxCell id="9fq4LfI0W2HX4gTsbRt6-5" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" parent="1" source="9fq4LfI0W2HX4gTsbRt6-4" target="9fq4LfI0W2HX4gTsbRt6-1" edge="1">
- <mxGeometry relative="1" as="geometry" />
- </mxCell>
- <mxCell id="9fq4LfI0W2HX4gTsbRt6-4" value="Play" style="text;html=1;align=right;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;" parent="1" vertex="1">
- <mxGeometry x="260" y="525" width="40" height="20" as="geometry" />
- </mxCell>
- <mxCell id="9fq4LfI0W2HX4gTsbRt6-7" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" parent="1" source="9fq4LfI0W2HX4gTsbRt6-6" target="9fq4LfI0W2HX4gTsbRt6-1" edge="1">
- <mxGeometry relative="1" as="geometry" />
- </mxCell>
- <mxCell id="9fq4LfI0W2HX4gTsbRt6-6" value="Pause" style="text;html=1;align=right;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;" parent="1" vertex="1">
- <mxGeometry x="250" y="562" width="50" height="20" as="geometry" />
- </mxCell>
- <mxCell id="9fq4LfI0W2HX4gTsbRt6-9" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" parent="1" source="9fq4LfI0W2HX4gTsbRt6-8" target="9fq4LfI0W2HX4gTsbRt6-1" edge="1">
- <mxGeometry relative="1" as="geometry" />
- </mxCell>
- <mxCell id="9fq4LfI0W2HX4gTsbRt6-8" value="Stop" style="text;html=1;align=right;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;" parent="1" vertex="1">
- <mxGeometry x="260" y="598" width="40" height="20" as="geometry" />
- </mxCell>
- <mxCell id="9fq4LfI0W2HX4gTsbRt6-11" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" parent="1" source="9fq4LfI0W2HX4gTsbRt6-10" target="9fq4LfI0W2HX4gTsbRt6-1" edge="1">
- <mxGeometry relative="1" as="geometry" />
- </mxCell>
- <mxCell id="9fq4LfI0W2HX4gTsbRt6-10" value="Set playback rate" style="text;html=1;align=right;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;" parent="1" vertex="1">
- <mxGeometry x="190" y="635" width="110" height="20" as="geometry" />
- </mxCell>
- <mxCell id="9fq4LfI0W2HX4gTsbRt6-13" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" parent="1" source="9fq4LfI0W2HX4gTsbRt6-12" target="9fq4LfI0W2HX4gTsbRt6-1" edge="1">
- <mxGeometry relative="1" as="geometry" />
- </mxCell>
- <mxCell id="9fq4LfI0W2HX4gTsbRt6-12" value="Set playback position" style="text;html=1;align=right;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;" parent="1" vertex="1">
- <mxGeometry x="170" y="671" width="130" height="20" as="geometry" />
- </mxCell>
- <mxCell id="5IB1TeDA8rQgVov2ilYq-11" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" parent="1" source="MU1YSbBTE73kW5gn9q-V-1" target="5IB1TeDA8rQgVov2ilYq-2" edge="1">
- <mxGeometry relative="1" as="geometry" />
- </mxCell>
- <mxCell id="MU1YSbBTE73kW5gn9q-V-1" value="Add a/b limit" style="text;html=1;align=right;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;" parent="1" vertex="1">
- <mxGeometry x="840" y="68" width="80" height="20" as="geometry" />
- </mxCell>
- <mxCell id="MU1YSbBTE73kW5gn9q-V-7" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.75;entryY=0;entryDx=0;entryDy=0;" parent="1" source="MU1YSbBTE73kW5gn9q-V-3" target="9fq4LfI0W2HX4gTsbRt6-1" edge="1">
- <mxGeometry relative="1" as="geometry" />
- </mxCell>
- <mxCell id="MU1YSbBTE73kW5gn9q-V-3" value="a/b controller" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" parent="1" vertex="1">
- <mxGeometry x="650" y="408" width="160" height="80" as="geometry" />
- </mxCell>
- <mxCell id="5IB1TeDA8rQgVov2ilYq-9" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" parent="1" source="MU1YSbBTE73kW5gn9q-V-9" target="MU1YSbBTE73kW5gn9q-V-3" edge="1">
- <mxGeometry relative="1" as="geometry" />
- </mxCell>
- <mxCell id="MU1YSbBTE73kW5gn9q-V-9" value="Enable a/b mode" style="text;html=1;align=left;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;" parent="1" vertex="1">
- <mxGeometry x="830" y="128" width="110" height="20" as="geometry" />
- </mxCell>
- <mxCell id="UmMSCIYVAIiNOvlXGdHZ-5" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" parent="1" source="MU1YSbBTE73kW5gn9q-V-11" target="MU1YSbBTE73kW5gn9q-V-3" edge="1">
- <mxGeometry relative="1" as="geometry" />
- </mxCell>
- <mxCell id="MU1YSbBTE73kW5gn9q-V-11" value="playlist" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" parent="1" vertex="1">
- <mxGeometry x="440" y="408" width="160" height="80" as="geometry" />
- </mxCell>
- <mxCell id="5IB1TeDA8rQgVov2ilYq-7" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" parent="1" source="MU1YSbBTE73kW5gn9q-V-13" target="5IB1TeDA8rQgVov2ilYq-2" edge="1">
- <mxGeometry relative="1" as="geometry" />
- </mxCell>
- <mxCell id="MU1YSbBTE73kW5gn9q-V-13" value="Add song" style="text;html=1;align=right;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;" parent="1" vertex="1">
- <mxGeometry x="360" y="98" width="70" height="20" as="geometry" />
- </mxCell>
- <mxCell id="5IB1TeDA8rQgVov2ilYq-8" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" parent="1" source="MU1YSbBTE73kW5gn9q-V-15" target="MU1YSbBTE73kW5gn9q-V-11" edge="1">
- <mxGeometry relative="1" as="geometry" />
- </mxCell>
- <mxCell id="MU1YSbBTE73kW5gn9q-V-15" value="Choose song" style="text;html=1;align=right;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;" parent="1" vertex="1">
- <mxGeometry x="340" y="128" width="90" height="20" as="geometry" />
- </mxCell>
- <mxCell id="MU1YSbBTE73kW5gn9q-V-18" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" parent="1" source="MU1YSbBTE73kW5gn9q-V-17" target="9fq4LfI0W2HX4gTsbRt6-1" edge="1">
- <mxGeometry relative="1" as="geometry" />
- </mxCell>
- <mxCell id="MU1YSbBTE73kW5gn9q-V-17" value="Set volume" style="text;html=1;align=right;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;" parent="1" vertex="1">
- <mxGeometry x="220" y="708" width="80" height="20" as="geometry" />
- </mxCell>
- <mxCell id="5IB1TeDA8rQgVov2ilYq-10" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" parent="1" source="MU1YSbBTE73kW5gn9q-V-22" target="MU1YSbBTE73kW5gn9q-V-3" edge="1">
- <mxGeometry relative="1" as="geometry" />
- </mxCell>
- <mxCell id="MU1YSbBTE73kW5gn9q-V-22" value="Choose a/b limit" style="text;html=1;align=left;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;" parent="1" vertex="1">
- <mxGeometry x="840" y="98" width="100" height="20" as="geometry" />
- </mxCell>
- <mxCell id="718ck8ZuCs3BOJF-nClt-4" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" parent="1" source="9fq4LfI0W2HX4gTsbRt6-1" target="718ck8ZuCs3BOJF-nClt-3" edge="1">
- <mxGeometry relative="1" as="geometry">
- <mxPoint x="760.3103448275863" y="608" as="targetPoint" />
- </mxGeometry>
- </mxCell>
- <mxCell id="718ck8ZuCs3BOJF-nClt-5" value="&lt;div&gt;Current&lt;/div&gt;&lt;div&gt;position&lt;/div&gt;" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="718ck8ZuCs3BOJF-nClt-4" vertex="1" connectable="0">
- <mxGeometry x="0.2833" relative="1" as="geometry">
- <mxPoint x="-23" as="offset" />
- </mxGeometry>
- </mxCell>
- <mxCell id="718ck8ZuCs3BOJF-nClt-6" value="&lt;div&gt;Set current&lt;/div&gt;&lt;div&gt;position&lt;br&gt;&lt;/div&gt;" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="718ck8ZuCs3BOJF-nClt-4" vertex="1" connectable="0">
- <mxGeometry x="0.2833" relative="1" as="geometry">
- <mxPoint x="-46" y="-120" as="offset" />
- </mxGeometry>
- </mxCell>
- <mxCell id="718ck8ZuCs3BOJF-nClt-3" value="tick" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
- <mxGeometry x="750" y="648" width="160" height="80" as="geometry" />
- </mxCell>
- <mxCell id="5IB1TeDA8rQgVov2ilYq-3" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;exitX=0.25;exitY=1;exitDx=0;exitDy=0;" parent="1" source="5IB1TeDA8rQgVov2ilYq-2" target="MU1YSbBTE73kW5gn9q-V-11" edge="1">
- <mxGeometry relative="1" as="geometry" />
- </mxCell>
- <mxCell id="5IB1TeDA8rQgVov2ilYq-4" value="Add song" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="5IB1TeDA8rQgVov2ilYq-3" vertex="1" connectable="0">
- <mxGeometry x="0.2933" y="3" relative="1" as="geometry">
- <mxPoint x="22" y="-4" as="offset" />
- </mxGeometry>
- </mxCell>
- <mxCell id="5IB1TeDA8rQgVov2ilYq-5" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.75;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" parent="1" source="5IB1TeDA8rQgVov2ilYq-2" target="MU1YSbBTE73kW5gn9q-V-3" edge="1">
- <mxGeometry relative="1" as="geometry" />
- </mxCell>
- <mxCell id="5IB1TeDA8rQgVov2ilYq-6" value="&lt;div&gt;Add a/b limit&lt;br&gt;&lt;/div&gt;" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="5IB1TeDA8rQgVov2ilYq-5" vertex="1" connectable="0">
- <mxGeometry x="0.2833" y="-2" relative="1" as="geometry">
- <mxPoint x="-32" y="-7" as="offset" />
- </mxGeometry>
- </mxCell>
- <mxCell id="5IB1TeDA8rQgVov2ilYq-2" value="&lt;div&gt;session&lt;/div&gt;&lt;div&gt;manager&lt;br&gt;&lt;/div&gt;" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" parent="1" vertex="1">
- <mxGeometry x="550" y="258" width="160" height="80" as="geometry" />
- </mxCell>
- <mxCell id="5IB1TeDA8rQgVov2ilYq-16" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" parent="1" source="5IB1TeDA8rQgVov2ilYq-14" target="5IB1TeDA8rQgVov2ilYq-2" edge="1">
- <mxGeometry relative="1" as="geometry" />
- </mxCell>
- <mxCell id="5IB1TeDA8rQgVov2ilYq-14" value="Load session" style="text;html=1;align=right;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;" parent="1" vertex="1">
- <mxGeometry x="445" y="58" width="90" height="20" as="geometry" />
- </mxCell>
- <mxCell id="5IB1TeDA8rQgVov2ilYq-17" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="5IB1TeDA8rQgVov2ilYq-15" edge="1">
- <mxGeometry relative="1" as="geometry">
- <mxPoint x="630" y="258" as="targetPoint" />
- </mxGeometry>
- </mxCell>
- <mxCell id="5IB1TeDA8rQgVov2ilYq-15" value="Save session" style="text;html=1;align=right;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;" parent="1" vertex="1">
- <mxGeometry x="445" y="28" width="90" height="20" as="geometry" />
- </mxCell>
- <mxCell id="PYF8YKytvgJsIEJjpRti-2" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=1;entryDx=0;entryDy=0;" parent="1" source="PYF8YKytvgJsIEJjpRti-1" target="718ck8ZuCs3BOJF-nClt-3" edge="1">
- <mxGeometry relative="1" as="geometry" />
- </mxCell>
- <mxCell id="PYF8YKytvgJsIEJjpRti-1" value="Tick" style="text;html=1;align=right;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;" parent="1" vertex="1">
- <mxGeometry x="700" y="940" width="40" height="20" as="geometry" />
- </mxCell>
- <mxCell id="ofcqv09syQELO3cvxpxf-3" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" parent="1" source="ofcqv09syQELO3cvxpxf-1" target="MU1YSbBTE73kW5gn9q-V-3" edge="1">
- <mxGeometry relative="1" as="geometry" />
- </mxCell>
- <mxCell id="ofcqv09syQELO3cvxpxf-1" value="Next a/b limit" style="text;html=1;align=left;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;" parent="1" vertex="1">
- <mxGeometry x="830" y="160" width="100" height="20" as="geometry" />
- </mxCell>
- <mxCell id="ofcqv09syQELO3cvxpxf-4" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" parent="1" source="ofcqv09syQELO3cvxpxf-2" target="MU1YSbBTE73kW5gn9q-V-3" edge="1">
- <mxGeometry relative="1" as="geometry" />
- </mxCell>
- <mxCell id="ofcqv09syQELO3cvxpxf-2" value="Previous a/b limit" style="text;html=1;align=left;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;" parent="1" vertex="1">
- <mxGeometry x="830" y="190" width="120" height="20" as="geometry" />
- </mxCell>
- <mxCell id="ofcqv09syQELO3cvxpxf-6" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" parent="1" source="ofcqv09syQELO3cvxpxf-5" target="9fq4LfI0W2HX4gTsbRt6-1" edge="1">
- <mxGeometry relative="1" as="geometry" />
- </mxCell>
- <mxCell id="ofcqv09syQELO3cvxpxf-5" value="Jump to A" style="text;html=1;align=right;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;" parent="1" vertex="1">
- <mxGeometry x="230" y="737" width="70" height="20" as="geometry" />
- </mxCell>
- <mxCell id="ofcqv09syQELO3cvxpxf-9" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" parent="1" source="ofcqv09syQELO3cvxpxf-7" target="MU1YSbBTE73kW5gn9q-V-11" edge="1">
- <mxGeometry relative="1" as="geometry" />
- </mxCell>
- <mxCell id="ofcqv09syQELO3cvxpxf-7" value="Next song" style="text;html=1;align=right;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;" parent="1" vertex="1">
- <mxGeometry x="350" y="160" width="80" height="20" as="geometry" />
- </mxCell>
- <mxCell id="ofcqv09syQELO3cvxpxf-10" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="ofcqv09syQELO3cvxpxf-8" edge="1">
- <mxGeometry relative="1" as="geometry">
- <mxPoint x="520" y="410" as="targetPoint" />
- </mxGeometry>
- </mxCell>
- <mxCell id="ofcqv09syQELO3cvxpxf-8" value="Previous song" style="text;html=1;align=right;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;" parent="1" vertex="1">
- <mxGeometry x="330" y="190" width="100" height="20" as="geometry" />
- </mxCell>
- <mxCell id="ofcqv09syQELO3cvxpxf-14" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;" parent="1" source="ofcqv09syQELO3cvxpxf-11" target="9fq4LfI0W2HX4gTsbRt6-1" edge="1">
- <mxGeometry relative="1" as="geometry" />
- </mxCell>
- <mxCell id="ofcqv09syQELO3cvxpxf-15" value="playing state&lt;br&gt;callback" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="ofcqv09syQELO3cvxpxf-14" vertex="1" connectable="0">
- <mxGeometry x="0.2283" y="-2" relative="1" as="geometry">
- <mxPoint x="22" as="offset" />
- </mxGeometry>
- </mxCell>
- <mxCell id="ofcqv09syQELO3cvxpxf-11" value="&lt;div&gt;notifier&lt;/div&gt;" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" parent="1" vertex="1">
- <mxGeometry x="505" y="790" width="160" height="80" as="geometry" />
- </mxCell>
- <mxCell id="ofcqv09syQELO3cvxpxf-13" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" parent="1" source="ofcqv09syQELO3cvxpxf-12" target="ofcqv09syQELO3cvxpxf-11" edge="1">
- <mxGeometry relative="1" as="geometry" />
- </mxCell>
- <mxCell id="ofcqv09syQELO3cvxpxf-12" value="Register playing state callback" style="text;html=1;align=right;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;" parent="1" vertex="1">
- <mxGeometry x="110" y="820" width="200" height="20" as="geometry" />
- </mxCell>
- </root>
- </mxGraphModel>
- </diagram>
- <diagram id="yK3rgzEW7m2RTtpwjvJ6" name="MIDI">
- <mxGraphModel dx="2731" dy="963" grid="0" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1169" pageHeight="827" math="0" shadow="0">
- <root>
- <mxCell id="OKDEixDBbmxQMGRGU1jO-0" />
- <mxCell id="OKDEixDBbmxQMGRGU1jO-1" parent="OKDEixDBbmxQMGRGU1jO-0" />
- <mxCell id="KjrEduvjUaLFBeyMDJhb-19" value="" style="endArrow=classic;html=1;rounded=0;entryX=0;entryY=0.25;entryDx=0;entryDy=0;" parent="OKDEixDBbmxQMGRGU1jO-1" edge="1">
- <mxGeometry width="50" height="50" relative="1" as="geometry">
- <mxPoint x="-270" y="247" as="sourcePoint" />
- <mxPoint x="-110" y="247" as="targetPoint" />
- </mxGeometry>
- </mxCell>
- <mxCell id="KjrEduvjUaLFBeyMDJhb-20" value="" style="endArrow=classic;html=1;rounded=0;exitX=0;exitY=0.75;exitDx=0;exitDy=0;" parent="OKDEixDBbmxQMGRGU1jO-1" edge="1">
- <mxGeometry width="50" height="50" relative="1" as="geometry">
- <mxPoint x="-110" y="280" as="sourcePoint" />
- <mxPoint x="-270" y="280.5" as="targetPoint" />
- </mxGeometry>
- </mxCell>
- <mxCell id="KjrEduvjUaLFBeyMDJhb-21" value="MIDI bus" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;" parent="OKDEixDBbmxQMGRGU1jO-1" vertex="1">
- <mxGeometry x="-230" y="255" width="70" height="20" as="geometry" />
- </mxCell>
- <mxCell id="KjrEduvjUaLFBeyMDJhb-22" value="Device" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="OKDEixDBbmxQMGRGU1jO-1" vertex="1">
- <mxGeometry x="-430" y="214.5" width="140" height="105.5" as="geometry" />
- </mxCell>
- <mxCell id="cDpx_x92aZCDt8U1TkIR-2" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.75;exitDx=0;exitDy=0;entryX=0;entryY=0.75;entryDx=0;entryDy=0;" parent="OKDEixDBbmxQMGRGU1jO-1" source="fBglSRjiR8ACvM9LEDBr-1" target="fBglSRjiR8ACvM9LEDBr-3" edge="1">
- <mxGeometry relative="1" as="geometry">
- <mxPoint x="80" y="294" as="targetPoint" />
- </mxGeometry>
- </mxCell>
- <mxCell id="cDpx_x92aZCDt8U1TkIR-3" value="callback" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="cDpx_x92aZCDt8U1TkIR-2" vertex="1" connectable="0">
- <mxGeometry x="-0.2773" y="-1" relative="1" as="geometry">
- <mxPoint x="10" as="offset" />
- </mxGeometry>
- </mxCell>
- <mxCell id="fBglSRjiR8ACvM9LEDBr-1" value="mido" style="rounded=0;whiteSpace=wrap;html=1;" parent="OKDEixDBbmxQMGRGU1jO-1" vertex="1">
- <mxGeometry x="-100" y="200" width="80" height="134.5" as="geometry" />
- </mxCell>
- <mxCell id="cDpx_x92aZCDt8U1TkIR-4" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.25;exitDx=0;exitDy=0;startArrow=classic;startFill=1;endArrow=none;endFill=0;" parent="OKDEixDBbmxQMGRGU1jO-1" source="fBglSRjiR8ACvM9LEDBr-3" edge="1">
- <mxGeometry relative="1" as="geometry">
- <mxPoint x="370" y="232.8888888888889" as="targetPoint" />
- </mxGeometry>
- </mxCell>
- <mxCell id="cDpx_x92aZCDt8U1TkIR-5" value="Set mapping" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="cDpx_x92aZCDt8U1TkIR-4" vertex="1" connectable="0">
- <mxGeometry x="-0.2097" y="-2" relative="1" as="geometry">
- <mxPoint x="17" y="-2" as="offset" />
- </mxGeometry>
- </mxCell>
- <mxCell id="cDpx_x92aZCDt8U1TkIR-6" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.75;exitDx=0;exitDy=0;startArrow=none;startFill=0;endArrow=classic;endFill=1;" parent="OKDEixDBbmxQMGRGU1jO-1" source="fBglSRjiR8ACvM9LEDBr-3" edge="1">
- <mxGeometry relative="1" as="geometry">
- <mxPoint x="360" y="300.66666666666663" as="targetPoint" />
- </mxGeometry>
- </mxCell>
- <mxCell id="cDpx_x92aZCDt8U1TkIR-7" value="Play/pause/stop&lt;br&gt;etc" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="cDpx_x92aZCDt8U1TkIR-6" vertex="1" connectable="0">
- <mxGeometry x="-0.26" y="-2" relative="1" as="geometry">
- <mxPoint x="14" y="-21" as="offset" />
- </mxGeometry>
- </mxCell>
- <mxCell id="FipM2rDAY6IncK5jYn7S-0" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;startArrow=classic;startFill=1;endArrow=none;endFill=0;" parent="OKDEixDBbmxQMGRGU1jO-1" source="fBglSRjiR8ACvM9LEDBr-3" edge="1">
- <mxGeometry relative="1" as="geometry">
- <mxPoint x="360" y="267.33333333333326" as="targetPoint" />
- </mxGeometry>
- </mxCell>
- <mxCell id="fBglSRjiR8ACvM9LEDBr-3" value="midi&lt;br&gt;interface" style="rounded=0;whiteSpace=wrap;html=1;" parent="OKDEixDBbmxQMGRGU1jO-1" vertex="1">
- <mxGeometry x="90" y="199.5" width="120" height="135.5" as="geometry" />
- </mxCell>
- <mxCell id="cDpx_x92aZCDt8U1TkIR-0" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=1;entryY=0.25;entryDx=0;entryDy=0;exitX=0;exitY=0.25;exitDx=0;exitDy=0;" parent="OKDEixDBbmxQMGRGU1jO-1" source="fBglSRjiR8ACvM9LEDBr-3" target="fBglSRjiR8ACvM9LEDBr-1" edge="1">
- <mxGeometry relative="1" as="geometry">
- <mxPoint x="80" y="241" as="sourcePoint" />
- <mxPoint x="210" y="530" as="targetPoint" />
- </mxGeometry>
- </mxCell>
- <mxCell id="cDpx_x92aZCDt8U1TkIR-1" value="send" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="cDpx_x92aZCDt8U1TkIR-0" vertex="1" connectable="0">
- <mxGeometry x="-0.168" y="5" relative="1" as="geometry">
- <mxPoint x="-10" y="-5" as="offset" />
- </mxGeometry>
- </mxCell>
- <mxCell id="FipM2rDAY6IncK5jYn7S-15" value="" style="group" parent="OKDEixDBbmxQMGRGU1jO-1" vertex="1" connectable="0">
- <mxGeometry x="717" y="125" width="190" height="429" as="geometry" />
- </mxCell>
- <mxCell id="jLuthnc1TARuY79bHbOS-0" value="&lt;div&gt;SoloTool&lt;/div&gt;" style="rounded=0;whiteSpace=wrap;html=1;glass=0;shadow=0;sketch=0;align=right;verticalAlign=top;" parent="FipM2rDAY6IncK5jYn7S-15" vertex="1">
- <mxGeometry width="190" height="429" as="geometry" />
- </mxCell>
- <mxCell id="jLuthnc1TARuY79bHbOS-7" value="Play" style="text;html=1;align=left;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;" parent="FipM2rDAY6IncK5jYn7S-15" vertex="1">
- <mxGeometry x="10" y="223" width="40" height="20" as="geometry" />
- </mxCell>
- <mxCell id="jLuthnc1TARuY79bHbOS-9" value="Pause" style="text;html=1;align=left;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;" parent="FipM2rDAY6IncK5jYn7S-15" vertex="1">
- <mxGeometry x="10" y="254" width="50" height="20" as="geometry" />
- </mxCell>
- <mxCell id="jLuthnc1TARuY79bHbOS-11" value="Stop" style="text;html=1;align=left;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;" parent="FipM2rDAY6IncK5jYn7S-15" vertex="1">
- <mxGeometry x="10" y="284" width="40" height="20" as="geometry" />
- </mxCell>
- <mxCell id="jLuthnc1TARuY79bHbOS-13" value="Set playback rate" style="text;html=1;align=left;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;" parent="FipM2rDAY6IncK5jYn7S-15" vertex="1">
- <mxGeometry x="10" y="315" width="110" height="20" as="geometry" />
- </mxCell>
- <mxCell id="jLuthnc1TARuY79bHbOS-15" value="Set playback position" style="text;html=1;align=left;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;fontColor=#666666;" parent="FipM2rDAY6IncK5jYn7S-15" vertex="1">
- <mxGeometry x="10" y="345" width="130" height="20" as="geometry" />
- </mxCell>
- <mxCell id="jLuthnc1TARuY79bHbOS-17" value="Add a/b limit" style="text;html=1;align=left;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;fontColor=#666666;" parent="FipM2rDAY6IncK5jYn7S-15" vertex="1">
- <mxGeometry x="10" y="71" width="80" height="20" as="geometry" />
- </mxCell>
- <mxCell id="jLuthnc1TARuY79bHbOS-21" value="Enable a/b mode" style="text;html=1;align=left;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;" parent="FipM2rDAY6IncK5jYn7S-15" vertex="1">
- <mxGeometry x="10" y="162" width="110" height="20" as="geometry" />
- </mxCell>
- <mxCell id="jLuthnc1TARuY79bHbOS-25" value="Add song" style="text;html=1;align=left;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;fontColor=#666666;" parent="FipM2rDAY6IncK5jYn7S-15" vertex="1">
- <mxGeometry x="10" y="101" width="70" height="20" as="geometry" />
- </mxCell>
- <mxCell id="jLuthnc1TARuY79bHbOS-27" value="Choose song" style="text;html=1;align=left;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;" parent="FipM2rDAY6IncK5jYn7S-15" vertex="1">
- <mxGeometry x="10" y="193" width="90" height="20" as="geometry" />
- </mxCell>
- <mxCell id="jLuthnc1TARuY79bHbOS-29" value="Set volume" style="text;html=1;align=left;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;" parent="FipM2rDAY6IncK5jYn7S-15" vertex="1">
- <mxGeometry x="10" y="376" width="80" height="20" as="geometry" />
- </mxCell>
- <mxCell id="jLuthnc1TARuY79bHbOS-31" value="Choose a/b limit" style="text;html=1;align=left;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;" parent="FipM2rDAY6IncK5jYn7S-15" vertex="1">
- <mxGeometry x="10" y="132" width="100" height="20" as="geometry" />
- </mxCell>
- <mxCell id="jLuthnc1TARuY79bHbOS-42" value="Load session" style="text;html=1;align=left;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;fontColor=#666666;" parent="FipM2rDAY6IncK5jYn7S-15" vertex="1">
- <mxGeometry x="10" y="40" width="90" height="20" as="geometry" />
- </mxCell>
- <mxCell id="jLuthnc1TARuY79bHbOS-44" value="Save session" style="text;html=1;align=left;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;fontColor=#666666;" parent="FipM2rDAY6IncK5jYn7S-15" vertex="1">
- <mxGeometry x="10" y="10" width="90" height="20" as="geometry" />
- </mxCell>
- <mxCell id="jLuthnc1TARuY79bHbOS-46" value="Tick" style="text;html=1;align=left;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;fontColor=#666666;" parent="FipM2rDAY6IncK5jYn7S-15" vertex="1">
- <mxGeometry x="10" y="406" width="40" height="20" as="geometry" />
- </mxCell>
- <mxCell id="FipM2rDAY6IncK5jYn7S-16" value="" style="group" parent="OKDEixDBbmxQMGRGU1jO-1" vertex="1" connectable="0">
- <mxGeometry x="517" y="255" width="110" height="232" as="geometry" />
- </mxCell>
- <mxCell id="FipM2rDAY6IncK5jYn7S-1" value="Play" style="text;html=1;align=left;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;" parent="FipM2rDAY6IncK5jYn7S-16" vertex="1">
- <mxGeometry y="91" width="40" height="20" as="geometry" />
- </mxCell>
- <mxCell id="FipM2rDAY6IncK5jYn7S-2" value="Pause" style="text;html=1;align=left;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;" parent="FipM2rDAY6IncK5jYn7S-16" vertex="1">
- <mxGeometry y="122" width="50" height="20" as="geometry" />
- </mxCell>
- <mxCell id="FipM2rDAY6IncK5jYn7S-3" value="Stop" style="text;html=1;align=left;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;" parent="FipM2rDAY6IncK5jYn7S-16" vertex="1">
- <mxGeometry y="152" width="40" height="20" as="geometry" />
- </mxCell>
- <mxCell id="FipM2rDAY6IncK5jYn7S-4" value="Set playback rate" style="text;html=1;align=left;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;" parent="FipM2rDAY6IncK5jYn7S-16" vertex="1">
- <mxGeometry y="183" width="110" height="20" as="geometry" />
- </mxCell>
- <mxCell id="FipM2rDAY6IncK5jYn7S-5" value="&lt;span style=&quot;color: rgb(0 , 0 , 0)&quot;&gt;Set volume&lt;/span&gt;" style="text;html=1;align=left;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;fontColor=#666666;" parent="FipM2rDAY6IncK5jYn7S-16" vertex="1">
- <mxGeometry y="214" width="78" height="18" as="geometry" />
- </mxCell>
- <mxCell id="FipM2rDAY6IncK5jYn7S-7" value="Enable a/b mode" style="text;html=1;align=left;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;" parent="FipM2rDAY6IncK5jYn7S-16" vertex="1">
- <mxGeometry y="30" width="110" height="20" as="geometry" />
- </mxCell>
- <mxCell id="FipM2rDAY6IncK5jYn7S-9" value="Choose song" style="text;html=1;align=left;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;" parent="FipM2rDAY6IncK5jYn7S-16" vertex="1">
- <mxGeometry y="61" width="90" height="20" as="geometry" />
- </mxCell>
- <mxCell id="FipM2rDAY6IncK5jYn7S-11" value="Choose a/b limit" style="text;html=1;align=left;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;" parent="FipM2rDAY6IncK5jYn7S-16" vertex="1">
- <mxGeometry width="100" height="20" as="geometry" />
- </mxCell>
- </root>
- </mxGraphModel>
- </diagram>
+<mxfile host="Electron" modified="2025-02-23T16:24:22.548Z" agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/22.1.2 Chrome/114.0.5735.289 Electron/25.9.4 Safari/537.36" etag="8sa4v4RLC4vbyHn2WtNr" version="22.1.2" type="device" pages="2">
<diagram id="PyNSc7ezSt42GBdjTBvd" name="Launchpad">
- <mxGraphModel dx="1562" dy="963" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1169" pageHeight="827" math="0" shadow="0">
+ <mxGraphModel dx="1561" dy="962" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1169" pageHeight="827" math="0" shadow="0">
<root>
<mxCell id="ZtjfeE3uwfRsFhnWfLYL-0" />
<mxCell id="ZtjfeE3uwfRsFhnWfLYL-1" parent="ZtjfeE3uwfRsFhnWfLYL-0" />
@@ -569,12 +200,12 @@
</mxGraphModel>
</diagram>
<diagram id="R-0UAU87gWX4lK6NCzNs" name="Web wireframe">
- <mxGraphModel dx="1562" dy="963" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1169" pageHeight="827" math="0" shadow="0">
+ <mxGraphModel dx="1561" dy="962" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1169" pageHeight="827" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="0goJ5iq8U8227kam6OUo-1" value="Song list" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
- <mxGeometry x="80" y="80" width="240" height="440" as="geometry" />
+ <mxGeometry x="80" y="80" width="240" height="360" as="geometry" />
</mxCell>
<mxCell id="0goJ5iq8U8227kam6OUo-2" value="Volume slider" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="320" y="80" width="440" height="40" as="geometry" />
@@ -589,55 +220,31 @@
<mxGeometry x="320" y="160" width="40" height="40" as="geometry" />
</mxCell>
<mxCell id="E9TFIlIWBKXALOEyTYeL-1" value="Seek slider" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
- <mxGeometry x="320" y="240" width="440" height="40" as="geometry" />
+ <mxGeometry x="390" y="240" width="300" height="40" as="geometry" />
</mxCell>
<mxCell id="E9TFIlIWBKXALOEyTYeL-2" value="Prev song" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
- <mxGeometry x="320" y="320" width="70" height="40" as="geometry" />
- </mxCell>
- <mxCell id="e6dzTLVl2QyovQL1D1hT-1" value="Seek&lt;br&gt;-25%" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
- <mxGeometry x="390" y="320" width="50" height="40" as="geometry" />
- </mxCell>
- <mxCell id="e6dzTLVl2QyovQL1D1hT-2" value="Seek&lt;br&gt;-5%" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
- <mxGeometry x="440" y="320" width="50" height="40" as="geometry" />
- </mxCell>
- <mxCell id="e6dzTLVl2QyovQL1D1hT-3" value="Seek&lt;br&gt;-1%" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
- <mxGeometry x="490" y="320" width="50" height="40" as="geometry" />
- </mxCell>
- <mxCell id="e6dzTLVl2QyovQL1D1hT-5" value="Seek&lt;br&gt;+1%" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
- <mxGeometry x="540" y="320" width="50" height="40" as="geometry" />
- </mxCell>
- <mxCell id="e6dzTLVl2QyovQL1D1hT-6" value="Seek&lt;br&gt;+5%" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
- <mxGeometry x="590" y="320" width="50" height="40" as="geometry" />
- </mxCell>
- <mxCell id="e6dzTLVl2QyovQL1D1hT-9" value="Seek&lt;br&gt;+25%" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
- <mxGeometry x="640" y="320" width="50" height="40" as="geometry" />
+ <mxGeometry x="320" y="240" width="70" height="40" as="geometry" />
</mxCell>
<mxCell id="e6dzTLVl2QyovQL1D1hT-10" value="Next song" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
- <mxGeometry x="690" y="320" width="70" height="40" as="geometry" />
+ <mxGeometry x="690" y="240" width="70" height="40" as="geometry" />
</mxCell>
- <mxCell id="2VOf0fCjGpZdvwMfuWx9-1" value="Set A" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
- <mxGeometry x="320" y="400" width="120" height="40" as="geometry" />
+ <mxCell id="2VOf0fCjGpZdvwMfuWx9-2" value="Set key point" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
+ <mxGeometry x="440" y="320" width="200" height="40" as="geometry" />
</mxCell>
- <mxCell id="2VOf0fCjGpZdvwMfuWx9-2" value="Set B" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
- <mxGeometry x="440" y="400" width="120" height="40" as="geometry" />
+ <mxCell id="eBDQebIGrGTMiAxLC66R-1" value="Previous" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
+ <mxGeometry x="320" y="320" width="120" height="40" as="geometry" />
</mxCell>
- <mxCell id="eBDQebIGrGTMiAxLC66R-1" value="Previous AB" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
- <mxGeometry x="560" y="400" width="100" height="40" as="geometry" />
- </mxCell>
- <mxCell id="eBDQebIGrGTMiAxLC66R-2" value="Next AB" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
- <mxGeometry x="660" y="400" width="100" height="40" as="geometry" />
- </mxCell>
- <mxCell id="e5je7AeTKV-z7aj2oazw-1" value="Toggle AB" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
- <mxGeometry x="320" y="480" width="80" height="40" as="geometry" />
+ <mxCell id="eBDQebIGrGTMiAxLC66R-2" value="Next" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
+ <mxGeometry x="640" y="320" width="120" height="40" as="geometry" />
</mxCell>
<mxCell id="e5je7AeTKV-z7aj2oazw-2" value="Stop" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
- <mxGeometry x="400" y="480" width="80" height="40" as="geometry" />
+ <mxGeometry x="320" y="400" width="120" height="40" as="geometry" />
</mxCell>
- <mxCell id="e5je7AeTKV-z7aj2oazw-3" value="Start" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
- <mxGeometry x="480" y="480" width="80" height="40" as="geometry" />
+ <mxCell id="e5je7AeTKV-z7aj2oazw-3" value="Play/pause" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
+ <mxGeometry x="600" y="400" width="160" height="40" as="geometry" />
</mxCell>
- <mxCell id="e5je7AeTKV-z7aj2oazw-4" value="Jump to A" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
- <mxGeometry x="560" y="480" width="200" height="40" as="geometry" />
+ <mxCell id="e5je7AeTKV-z7aj2oazw-4" value="Jump" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
+ <mxGeometry x="440" y="400" width="160" height="40" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
diff --git a/readme.md b/readme.md
index b5d1094..20f15b6 100644
--- a/readme.md
+++ b/readme.md
@@ -1,6 +1,6 @@
# Solo Tool
-This tool is designed to facilitate learning songs, and solos in particular, by slowing down playback and automatically repeating short sections of the file.
+This tool is designed to facilitate learning songs, and solos in particular, by slowing down playback and automatically jumping to predefined points in the song.
## Dependencies
diff --git a/solo-tool-project/pyproject.toml b/solo-tool-project/pyproject.toml
index 36d4891..7921be9 100644
--- a/solo-tool-project/pyproject.toml
+++ b/solo-tool-project/pyproject.toml
@@ -4,18 +4,18 @@ build-backend = "setuptools.build_meta"
[project]
name = "solo_tool"
+version = "2.0"
authors = [
{ name = "Eddy Pedroni", email = "epedroni@pm.me" },
]
description = "A library for dissecting guitar solos"
-requires-python = ">=3.12"
+requires-python = ">=3.13"
dependencies = [
"python-rtmidi",
"sip",
"mido",
"python-vlc"
]
-dynamic = ["version"]
[project.optional-dependencies]
dev = [
diff --git a/solo-tool-project/src/solo_tool/handlers.py b/solo-tool-project/src/solo_tool/handlers.py
index 13e982b..1e0e22c 100644
--- a/solo-tool-project/src/solo_tool/handlers.py
+++ b/solo-tool-project/src/solo_tool/handlers.py
@@ -2,6 +2,14 @@ from collections.abc import Callable
from solo_tool.solo_tool import SoloTool
+def playPause(st: SoloTool) -> Callable[[], None]:
+ def f():
+ if st.playing:
+ st.pause()
+ else:
+ st.play()
+ return f
+
def changeSong(st: SoloTool, delta: int) -> Callable[[], None]:
def f():
if st.song is None:
@@ -19,3 +27,37 @@ def positionToKeyPoint(st: SoloTool) -> Callable[[], None]:
def f():
st.keyPoint = st.position
return f
+
+def setKeyPoint(st: SoloTool, kp: float) -> Callable[[], None]:
+ def f():
+ st.keyPoint = kp
+ return f
+
+def changeKeyPoint(st: SoloTool, delta: int) -> Callable[[], None]:
+ from bisect import bisect_right, bisect_left
+ def f():
+ if delta > 0:
+ pivot = bisect_right(st.keyPoints, st.keyPoint) - 1
+ elif delta < 0:
+ pivot = bisect_left(st.keyPoints, st.keyPoint) - 1
+ else:
+ return
+ new = max(min(pivot + delta, len(st.keyPoints) - 1), 0)
+ st.keyPoint = st.keyPoints[new]
+ return f
+
+def rateAbsolute(st: SoloTool, value: float) -> Callable[[], None]:
+ def f():
+ st.rate = value
+ return f
+
+def rateRelative(st: SoloTool, delta: float) -> Callable[[], None]:
+ def f():
+ st.rate += delta
+ return f
+
+def volumeAbsolute(st: SoloTool, value: float) -> Callable[[], None]:
+ def f():
+ st.volume = value
+ return f
+
diff --git a/solo-tool-project/src/solo_tool/midi_controller_launchpad_mini.py b/solo-tool-project/src/solo_tool/midi_controller_launchpad_mini.py
index 3dc8ec6..38b7cce 100644
--- a/solo-tool-project/src/solo_tool/midi_controller_launchpad_mini.py
+++ b/solo-tool-project/src/solo_tool/midi_controller_launchpad_mini.py
@@ -34,9 +34,9 @@ class MidiController:
self._handlers = {
96 : self._soloTool.stop,
114 : self._soloTool.jump,
- 112 : self._playPause,
- #118 : self._soloTool.previousStoredAbLimits,
- #119 : self._soloTool.nextStoredAbLimits,
+ 112 : handlers.playPause(self._soloTool),
+ 118 : handlers.changeKeyPoint(self._soloTool, -1),
+ 119 : handlers.changeKeyPoint(self._soloTool, 1),
117 : handlers.positionToKeyPoint(self._soloTool),
48 : handlers.changeSong(self._soloTool, -1),
49 : handlers.seekRelative(self._soloTool, -0.25),
@@ -50,18 +50,18 @@ class MidiController:
for i in range(0, 8):
volume = round(MidiController.MIN_PLAYBACK_VOLUME + MidiController.PLAYBACK_VOLUME_STEP * i, 1)
- self._handlers[i] = self._createSetPlaybackVolumeCallback(volume)
+ self._handlers[i] = handlers.volumeAbsolute(self._soloTool, volume)
for i, button in enumerate(range(16, 24)):
rate = round(MidiController.MIN_PLAYBACK_RATE + MidiController.PLAYBACK_RATE_STEP * i, 1)
- self._handlers[button] = self._createSetPlaybackRateCallback(rate)
+ self._handlers[button] = handlers.rateAbsolute(self._soloTool, rate)
def connect(self):
self._midiWrapper.connect(MidiController.DEVICE_NAME, self._callback)
self._initialiseButtonLEDs()
def disconnect(self):
- self._allLEDsOff()
+ self._setAllLEDs(MidiController.LED_OFF)
self._midiWrapper.disconnect()
def _callback(self, msg):
@@ -71,24 +71,12 @@ class MidiController:
if msg.note in self._handlers:
handler = self._handlers[msg.note]()
- def _playPause(self):
- if self._soloTool.isPlaying():
- self._soloTool.pause()
- else:
- self._soloTool.play()
-
def _updatePlayPauseButton(self, playing):
if playing:
self._setButtonLED(7, 0, MidiController.LED_GREEN)
else:
self._setButtonLED(7, 0, MidiController.LED_YELLOW)
- def _updateToggleAbLimitEnableButton(self, enabled):
- if enabled:
- self._setButtonLED(6, 2, MidiController.LED_GREEN)
- else:
- self._setButtonLED(6, 2, MidiController.LED_RED)
-
def _updateVolumeRow(self, volume):
t1 = int(round(volume / MidiController.PLAYBACK_VOLUME_STEP, 1))
t2 = int(round(MidiController.MIN_PLAYBACK_VOLUME / MidiController.PLAYBACK_VOLUME_STEP, 1))
@@ -101,16 +89,6 @@ class MidiController:
lastColumnLit = t1 - t2 + 1
self._lightRowUntilColumn(1, lastColumnLit, MidiController.LED_YELLOW)
- def _createSetPlaybackRateCallback(self, rate):
- def f():
- self._soloTool.rate = rate
- return f
-
- def _createSetPlaybackVolumeCallback(self, volume):
- def f():
- self._soloTool.volume = volume
- return f
-
def _setButtonLED(self, row, col, colour):
self._midiWrapper.sendMessage(MidiController.BUTTON_MATRIX[row][col], colour, MidiController.LIGHT_CONTROL_CHANNEL)
@@ -119,13 +97,13 @@ class MidiController:
for col in range(0, 8):
self._setButtonLED(row, col, colours[col])
- def _allLEDsOff(self):
+ def _setAllLEDs(self, colour):
for row in range(0, 8):
for col in range(0, 8):
- self._setButtonLED(row, col, MidiController.LED_OFF)
+ self._setButtonLED(row, col, colour)
def _initialiseButtonLEDs(self):
- self._allLEDsOff()
+ self._setAllLEDs(MidiController.LED_OFF)
# volume buttons
self._updateVolumeRow(self._soloTool.volume)
@@ -135,9 +113,9 @@ class MidiController:
# playback control
self._setButtonLED(6, 0, MidiController.LED_RED)
- self._updatePlayPauseButton(self._soloTool.isPlaying())
+ self._updatePlayPauseButton(self._soloTool.playing)
- # AB control
+ # Key point control
self._setButtonLED(7, 2, MidiController.LED_YELLOW)
self._setButtonLED(7, 6, MidiController.LED_RED)
self._setButtonLED(7, 7, MidiController.LED_GREEN)
diff --git a/solo-tool-project/src/solo_tool/solo_tool.py b/solo-tool-project/src/solo_tool/solo_tool.py
index 147a7b9..0f47aef 100644
--- a/solo-tool-project/src/solo_tool/solo_tool.py
+++ b/solo-tool-project/src/solo_tool/solo_tool.py
@@ -29,7 +29,7 @@ class SoloTool:
def addSong(self, path: str) -> None:
if not os.path.isfile(path):
- raise FileNotFoundError()
+ raise FileNotFoundError(path)
self._songs.append(path)
self._keyPoints.append([])
@@ -76,7 +76,8 @@ class SoloTool:
def stop(self):
self._player.stop()
- def isPlaying(self):
+ @property
+ def playing(self) -> bool:
return self._player.isPlaying()
def jump(self):
@@ -108,7 +109,6 @@ class SoloTool:
@position.setter
def position(self, new: float) -> None:
- # TODO stop playback before changing position?
if new is not None and new != self._player.getPlaybackPosition():
self._player.setPlaybackPosition(min(max(0.0, new), 1.0))
diff --git a/solo-tool-project/test/midi_launchpad_mini_integrationtest.py b/solo-tool-project/test/midi_launchpad_mini_integrationtest.py
index 9588f9f..9e8c92e 100644
--- a/solo-tool-project/test/midi_launchpad_mini_integrationtest.py
+++ b/solo-tool-project/test/midi_launchpad_mini_integrationtest.py
@@ -167,16 +167,16 @@ def test_previousAndNextKeyPositionButtons(uut, midiWrapperMock, soloTool, playe
assert soloTool.keyPoint == 0.0
midiWrapperMock.simulateInput(nextKeyPositionButton)
- soloTool.keyPoint == 0.1
+ assert soloTool.keyPoint == 0.1
midiWrapperMock.simulateInput(nextKeyPositionButton)
- soloTool.keyPoint == 0.2
+ assert soloTool.keyPoint == 0.2
midiWrapperMock.simulateInput(previousKeyPositionButton)
- soloTool.keyPoint == 0.1
+ assert soloTool.keyPoint == 0.1
midiWrapperMock.simulateInput(previousKeyPositionButton)
- soloTool.keyPoint == 0.1
+ assert soloTool.keyPoint == 0.1
def test_playbackRateButtons(uut, midiWrapperMock, soloTool, playerMock):
playbackRateOptions = {
diff --git a/solo-tool-project/test/solo_tool_integrationtest.py b/solo-tool-project/test/solo_tool_integrationtest.py
index 2a818ed..2a55df9 100644
--- a/solo-tool-project/test/solo_tool_integrationtest.py
+++ b/solo-tool-project/test/solo_tool_integrationtest.py
@@ -27,16 +27,16 @@ def prepared_tmp_path(tmp_path):
def test_playerControls(uut, mockPlayer):
assert mockPlayer.state == MockPlayer.STOPPED
- assert uut.isPlaying() == False
+ assert uut.playing == False
uut.play()
assert mockPlayer.state == MockPlayer.PLAYING
- assert uut.isPlaying() == True
+ assert uut.playing == True
uut.pause()
assert mockPlayer.state == MockPlayer.PAUSED
- assert uut.isPlaying() == False
+ assert uut.playing == False
uut.stop()
assert mockPlayer.state == MockPlayer.STOPPED
- assert uut.isPlaying() == False
+ assert uut.playing == False
assert mockPlayer.rate == 1.0
uut.rate = 0.5
diff --git a/web-project/pyproject.toml b/web-project/pyproject.toml
index 440812e..844de72 100644
--- a/web-project/pyproject.toml
+++ b/web-project/pyproject.toml
@@ -8,10 +8,10 @@ authors = [
{ name = "Eddy Pedroni", email = "epedroni@pm.me" },
]
description = "A NiceGUI-based web frontend for the solo_tool library"
-requires-python = ">=3.12"
+requires-python = ">=3.13"
dependencies = [
"nicegui==2.11.1",
- "solo_tool"
+ "solo_tool>=2.0"
]
dynamic = ["version"]
diff --git a/web-project/src/solo_tool_web.py b/web-project/src/solo_tool_web.py
index f854e1a..edca008 100644
--- a/web-project/src/solo_tool_web.py
+++ b/web-project/src/solo_tool_web.py
@@ -1,56 +1,61 @@
from nicegui import ui
from solo_tool import SoloTool
+from solo_tool.session_manager import loadSession
+from solo_tool import handlers
+from solo_tool.midi_controller_launchpad_mini import MidiController
-st = SoloTool()
-st.loadSession("/home/eddy/music/solos/practice.json")
-
-def _createSeekHandler(delta):
- def f():
- newPosition = st.getPlaybackPosition() + delta
- newPosition = min(1.0, max(0.0, newPosition))
- st.setPlaybackPosition(newPosition)
- return f
+st = loadSession("/home/eddy/music/funk-band/practice.json")
+midiController = MidiController(st)
+midiController.connect()
def main():
- with ui.splitter(value=30) as splitter:
- splitter.style('width: 100%; height: 100%;')
- with splitter.before:
- with ui.list().props('dense separator'):
- for song in st.getSongs():
- ui.item(song)
- with splitter.after:
- ui.slider(min=0, max=1.2, value=1.0, step=0.01, on_change=lambda e: st.setPlaybackVolume(e.value))
-
- with ui.row().classes("w-full justify-between no-wrap"):
- ui.button('-5%', on_click=lambda: st.setPlaybackRate(max(0.5, st.getPlaybackRate() - 0.05)))
- ui.slider(min=0.5, max=1.2, step=0.05, value=st.getPlaybackRate(), on_change=lambda e: st.setPlaybackRate(e.value))
- ui.button('+5%', on_click=lambda: st.setPlaybackRate(min(1.2, st.getPlaybackRate() + 0.05)))
-
- ui.slider(min=0, max=100, value=0)
-
- with ui.row().classes("w-full justify-between no-wrap"):
- ui.button('Prev', on_click=st.previousSong)
- ui.button('-25%', on_click=_createSeekHandler(-0.25))
- ui.button('-5%', on_click=_createSeekHandler(-0.05))
- ui.button('-1%', on_click=_createSeekHandler(-0.01))
- ui.button('+1%', on_click=_createSeekHandler(0.01))
- ui.button('+5%', on_click=_createSeekHandler(0.05))
- ui.button('+25%', on_click=_createSeekHandler(0.25))
- ui.button('Next', on_click=st.nextSong)
-
- with ui.row().classes("w-full justify-between no-wrap"):
- ui.button('Set A')
- ui.button('Set B')
- ui.button('Previous AB')
- ui.button('Next AB')
-
- with ui.row().classes("w-full justify-between no-wrap"):
- ui.button('Toggle AB', on_click=lambda: st.setAbLimitEnable(not st.isAbLimitEnabled()))
- ui.button('Stop', on_click=st.stop)
- ui.button('Play', on_click=st.play)
- ui.button('Jump to A', on_click=st.jumpToA)
- ui.run()
+ fullscreen = ui.fullscreen()
+ ui.dark_mode().enable()
+ ui.colors(secondary='#ffc107')
+
+ with ui.row() as mainContainer:
+ mainContainer.style('width: 100%; height: 100%;')
+
+ with ui.column() as keyPointColumn:
+ keyPointColumn.style('width: 12%; height: 100%;')
+
+ ui.label('Key Points')
+
+ with ui.scroll_area():
+ for i, song in enumerate(st.songs):
+ with ui.list().props('dense separator').bind_visibility_from(st, 'song', value=i) as keyPointList:
+ for kp in st._keyPoints[i]:
+ ui.item(f"{kp}", on_click=handlers.setKeyPoint(st, kp)).props('clickable')
+
+ with ui.column() as controlColumn:
+ controlColumn.style('width: 84%; height: 100%;')
+
+ ui.label("Song name").bind_text_from(st, 'song', lambda index: st.songs[index] if index is not None else "")
+
+ # Playback position
+ ui.slider(min=0, max=1.0, step=0.01).bind_value(st, 'position').props('thumb-size=0px track-size=16px')
+
+ # Key point position
+ ui.slider(min=0, max=1.0, step=0.01).bind_value(st, 'keyPoint').props('color=secondary')
+
+ # Play control
+ with ui.button_group().classes('w-full').style('height: 90px'):
+ ui.button(icon='skip_previous', on_click=handlers.changeSong(st, -1)).style('flex: 1')
+ ui.button(icon='stop', on_click=st.stop, color='negative').style('flex: 1')
+ ui.button(color='positive', on_click=handlers.playPause(st)).bind_icon_from(st, "playing", lambda playing: "pause" if playing else "play_arrow").style('flex: 2')
+ ui.button(icon='undo', on_click=st.jump, color='secondary').style('flex: 2')
+ ui.button(icon='skip_next', on_click=handlers.changeSong(st, 1)).style('flex: 1')
+
+ # Volume and rate
+ with ui.row().classes('w-full justify-between no-wrap'):
+ ui.button(icon='fast_rewind', on_click=handlers.rateRelative(st, -0.05))
+ ui.label().bind_text_from(st, 'rate', lambda rate: f'{rate:0.3}').on('click', handlers.rateAbsolute(st, 1.0))
+ ui.button(icon='fast_forward', on_click=handlers.rateRelative(st, 0.05))
+ ui.slider(min=0, max=1.2, step=0.01).bind_value(st, 'volume')
+ ui.button(icon='fullscreen', on_click=fullscreen.toggle).props('outline')
+
+ ui.run()
if __name__ in {'__main__', '__mp_main__'}:
main()