aboutsummaryrefslogtreecommitdiffstats
path: root/solo-tool-project
diff options
context:
space:
mode:
authorEddy Pedroni <epedroni@pm.me>2025-08-21 18:56:43 +0200
committerEddy Pedroni <epedroni@pm.me>2025-08-21 18:56:43 +0200
commit4ea8344fba863d3ff113cf790b6327d44ced62ee (patch)
tree0db18a3b5039258567d36d782687b83ed0ff9baa /solo-tool-project
parent748f056faf16b08ac41de991b1aeb664f2b86d8e (diff)
Actition controller prototype
Diffstat (limited to 'solo-tool-project')
-rw-r--r--solo-tool-project/src/solo_tool/handlers.py4
-rw-r--r--solo-tool-project/src/solo_tool/midi_controller_actition.py43
-rw-r--r--solo-tool-project/src/solo_tool/midi_controller_launchpad_mini.py89
-rw-r--r--solo-tool-project/src/solo_tool/midi_wrapper_mido.py28
-rw-r--r--solo-tool-project/test/midi_actition_pedal_integrationtest.py118
-rw-r--r--solo-tool-project/test/midi_launchpad_mini_integrationtest.py18
-rw-r--r--solo-tool-project/test/player_mock.py2
7 files changed, 232 insertions, 70 deletions
diff --git a/solo-tool-project/src/solo_tool/handlers.py b/solo-tool-project/src/solo_tool/handlers.py
index 1820e86..3beb0fb 100644
--- a/solo-tool-project/src/solo_tool/handlers.py
+++ b/solo-tool-project/src/solo_tool/handlers.py
@@ -38,9 +38,9 @@ def seekRelative(st: SoloTool, delta: float) -> Callable[[], None]:
st.position += delta
return f
-def seekAbsolute(st: SoloTool, delta: float) -> Callable[[], None]:
+def seekAbsolute(st: SoloTool, new: float) -> Callable[[], None]:
def f():
- st.position = delta
+ st.position = new
return f
def positionToKeyPoint(st: SoloTool) -> Callable[[], None]:
diff --git a/solo-tool-project/src/solo_tool/midi_controller_actition.py b/solo-tool-project/src/solo_tool/midi_controller_actition.py
new file mode 100644
index 0000000..184b9c8
--- /dev/null
+++ b/solo-tool-project/src/solo_tool/midi_controller_actition.py
@@ -0,0 +1,43 @@
+import mido
+from collections.abc import Callable
+
+from . import handlers
+from .solo_tool import SoloTool
+
+class ActitionController:
+ class _MidoMidiWrapper:
+ def __init__(self):
+ self._callback = None
+ self._inPort = mido.open_input("f_midi")
+ self._inPort.callback = self._midoCallback
+
+ def setCallback(self, callback: Callable[[int, int], None]) -> None:
+ self._callback = callback
+
+ def _midoCallback(msg: mido.Message) -> None:
+ if msg.type != "control_change":
+ return
+ if self._callback:
+ self._callback(msg.control, msg.channel)
+
+ def __init__(self, midiWrapperOverride=None):
+ self._handlers = {}
+ if midiWrapperOverride:
+ self._midiWrapper = midiWrapperOverride
+ else:
+ self._midiWrapper = ActitionController._MidoMidiWrapper()
+ self._midiWrapper.setCallback(self._callback)
+
+ def _callback(self, control: int, channel: int) -> None:
+ if channel != 15:
+ return
+ if control in self._handlers:
+ self._handlers[control]()
+
+ def setSoloTool(self, soloTool: SoloTool) -> None:
+ self._handlers = {
+ 102: handlers.seekAbsolute(soloTool, 0.0),
+ 103: handlers.positionToKeyPoint(soloTool),
+ 104: soloTool.jump,
+ 105: handlers.playPause(soloTool)
+ }
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 625e2ef..e79b60c 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
@@ -1,7 +1,34 @@
-from .midi_wrapper_mido import MidiWrapper
+import mido
from . import handlers
+from .solo_tool import SoloTool
-class MidiController:
+class MidiWrapper:
+ def __init__(self):
+ self._inPort = None
+ self._outPort = None
+
+ def connect(self, deviceName, callback):
+ if self._inPort is None and self._outPort is None:
+ self._inPort = mido.open_input(deviceName)
+ self._inPort.callback = callback
+ self._outPort = mido.open_output(deviceName)
+
+ def disconnect(self):
+ if self._inPort is not None:
+ self._inPort.close()
+ self._inPort = None
+
+ if self._outPort is not None:
+ self._outPort.reset()
+ self._outPort.close()
+ self._outPort = None
+
+ def sendNoteOn(self, note, velocity, channel):
+ if self._outPort is not None:
+ msg = mido.Message("note_on", channel=channel, velocity=velocity, note=note)
+ self._outPort.send(msg)
+
+class LaunchpadMiniController:
DEVICE_NAME = "Launchpad Mini MIDI 1"
LIGHT_CONTROL_CHANNEL = 0
LED_GREEN = 124
@@ -18,7 +45,7 @@ class MidiController:
MAX_PLAYBACK_VOLUME = 1.2
PLAYBACK_VOLUME_STEP = 0.1
- def __init__(self, soloTool, midiWrapperOverride=None):
+ def __init__(self, soloTool: SoloTool, midiWrapperOverride=None):
self._soloTool = soloTool
if midiWrapperOverride is not None:
self._midiWrapper = midiWrapperOverride
@@ -49,19 +76,19 @@ class MidiController:
}
for i in range(0, 8):
- volume = round(MidiController.MIN_PLAYBACK_VOLUME + MidiController.PLAYBACK_VOLUME_STEP * i, 1)
+ volume = round(LaunchpadMiniController.MIN_PLAYBACK_VOLUME + LaunchpadMiniController.PLAYBACK_VOLUME_STEP * i, 1)
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)
+ rate = round(LaunchpadMiniController.MIN_PLAYBACK_RATE + LaunchpadMiniController.PLAYBACK_RATE_STEP * i, 1)
self._handlers[button] = handlers.rateAbsolute(self._soloTool, rate)
def connect(self):
- self._midiWrapper.connect(MidiController.DEVICE_NAME, self._callback)
+ self._midiWrapper.connect(LaunchpadMiniController.DEVICE_NAME, self._callback)
self._initialiseButtonLEDs()
def disconnect(self):
- self._setAllLEDs(MidiController.LED_OFF)
+ self._setAllLEDs(LaunchpadMiniController.LED_OFF)
self._midiWrapper.disconnect()
def _callback(self, msg):
@@ -73,27 +100,27 @@ class MidiController:
def _updatePlayPauseButton(self, playing):
if playing:
- self._setButtonLED(7, 0, MidiController.LED_GREEN)
+ self._setButtonLED(7, 0, LaunchpadMiniController.LED_GREEN)
else:
- self._setButtonLED(7, 0, MidiController.LED_YELLOW)
+ self._setButtonLED(7, 0, LaunchpadMiniController.LED_YELLOW)
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))
+ t1 = int(round(volume / LaunchpadMiniController.PLAYBACK_VOLUME_STEP, 1))
+ t2 = int(round(LaunchpadMiniController.MIN_PLAYBACK_VOLUME / LaunchpadMiniController.PLAYBACK_VOLUME_STEP, 1))
lastColumnLit = t1 - t2 + 1
- self._lightRowUntilColumn(0, lastColumnLit, MidiController.LED_GREEN)
+ self._lightRowUntilColumn(0, lastColumnLit, LaunchpadMiniController.LED_GREEN)
def _updateRateRow(self, rate):
- t1 = int(round(rate / MidiController.PLAYBACK_RATE_STEP, 1))
- t2 = int(round(MidiController.MIN_PLAYBACK_RATE / MidiController.PLAYBACK_RATE_STEP, 1))
+ t1 = int(round(rate / LaunchpadMiniController.PLAYBACK_RATE_STEP, 1))
+ t2 = int(round(LaunchpadMiniController.MIN_PLAYBACK_RATE / LaunchpadMiniController.PLAYBACK_RATE_STEP, 1))
lastColumnLit = t1 - t2 + 1
- self._lightRowUntilColumn(1, lastColumnLit, MidiController.LED_YELLOW)
+ self._lightRowUntilColumn(1, lastColumnLit, LaunchpadMiniController.LED_YELLOW)
def _setButtonLED(self, row, col, colour):
- self._midiWrapper.sendMessage(MidiController.BUTTON_MATRIX[row][col], colour, MidiController.LIGHT_CONTROL_CHANNEL)
+ self._midiWrapper.sendNoteOn(LaunchpadMiniController.BUTTON_MATRIX[row][col], colour, LaunchpadMiniController.LIGHT_CONTROL_CHANNEL)
def _lightRowUntilColumn(self, row, column, litColour):
- colours = [litColour] * column + [MidiController.LED_OFF] * (8 - column)
+ colours = [litColour] * column + [LaunchpadMiniController.LED_OFF] * (8 - column)
for col in range(0, 8):
self._setButtonLED(row, col, colours[col])
@@ -103,7 +130,7 @@ class MidiController:
self._setButtonLED(row, col, colour)
def _initialiseButtonLEDs(self):
- self._setAllLEDs(MidiController.LED_OFF)
+ self._setAllLEDs(LaunchpadMiniController.LED_OFF)
# volume buttons
self._updateVolumeRow(self._soloTool.volume)
@@ -112,22 +139,22 @@ class MidiController:
self._updateRateRow(self._soloTool.rate)
# playback control
- self._setButtonLED(6, 0, MidiController.LED_YELLOW)
+ self._setButtonLED(6, 0, LaunchpadMiniController.LED_YELLOW)
self._updatePlayPauseButton(self._soloTool.playing)
# Key point control
- self._setButtonLED(7, 2, MidiController.LED_YELLOW)
- self._setButtonLED(7, 6, MidiController.LED_RED)
- self._setButtonLED(7, 7, MidiController.LED_GREEN)
- self._setButtonLED(7, 5, MidiController.LED_YELLOW)
+ self._setButtonLED(7, 2, LaunchpadMiniController.LED_YELLOW)
+ self._setButtonLED(7, 6, LaunchpadMiniController.LED_RED)
+ self._setButtonLED(7, 7, LaunchpadMiniController.LED_GREEN)
+ self._setButtonLED(7, 5, LaunchpadMiniController.LED_YELLOW)
# Song control
- self._setButtonLED(3, 0, MidiController.LED_RED)
- self._setButtonLED(3, 1, MidiController.LED_RED)
- self._setButtonLED(3, 2, MidiController.LED_RED)
- self._setButtonLED(3, 3, MidiController.LED_RED)
- self._setButtonLED(3, 4, MidiController.LED_GREEN)
- self._setButtonLED(3, 5, MidiController.LED_GREEN)
- self._setButtonLED(3, 6, MidiController.LED_GREEN)
- self._setButtonLED(3, 7, MidiController.LED_GREEN)
+ self._setButtonLED(3, 0, LaunchpadMiniController.LED_RED)
+ self._setButtonLED(3, 1, LaunchpadMiniController.LED_RED)
+ self._setButtonLED(3, 2, LaunchpadMiniController.LED_RED)
+ self._setButtonLED(3, 3, LaunchpadMiniController.LED_RED)
+ self._setButtonLED(3, 4, LaunchpadMiniController.LED_GREEN)
+ self._setButtonLED(3, 5, LaunchpadMiniController.LED_GREEN)
+ self._setButtonLED(3, 6, LaunchpadMiniController.LED_GREEN)
+ self._setButtonLED(3, 7, LaunchpadMiniController.LED_GREEN)
diff --git a/solo-tool-project/src/solo_tool/midi_wrapper_mido.py b/solo-tool-project/src/solo_tool/midi_wrapper_mido.py
deleted file mode 100644
index 34f1031..0000000
--- a/solo-tool-project/src/solo_tool/midi_wrapper_mido.py
+++ /dev/null
@@ -1,28 +0,0 @@
-import mido
-
-class MidiWrapper:
- def __init__(self):
- self._inPort = None
- self._outPort = None
-
- def connect(self, deviceName, callback):
- if self._inPort is None and self._outPort is None:
- self._inPort = mido.open_input(deviceName)
- self._inPort.callback = callback
- self._outPort = mido.open_output(deviceName)
-
- def disconnect(self):
- if self._inPort is not None:
- self._inPort.close()
- self._inPort = None
-
- if self._outPort is not None:
- self._outPort.reset()
- self._outPort.close()
- self._outPort = None
-
- def sendMessage(self, note, velocity, channel):
- if self._outPort is not None:
- msg = mido.Message('note_on', channel=channel, velocity=velocity, note=note)
- self._outPort.send(msg)
-
diff --git a/solo-tool-project/test/midi_actition_pedal_integrationtest.py b/solo-tool-project/test/midi_actition_pedal_integrationtest.py
new file mode 100644
index 0000000..092ff64
--- /dev/null
+++ b/solo-tool-project/test/midi_actition_pedal_integrationtest.py
@@ -0,0 +1,118 @@
+import pytest
+from fixtures import mockPlayer, testSongs
+from solo_tool.solo_tool import SoloTool
+from solo_tool.midi_controller_actition import ActitionController
+
+CHANNEL = 15
+REWIND = 102
+SET = 103
+JUMP = 104
+PLAY = 105
+
+class MidiWrapperMock:
+ def __init__(self):
+ self.sentMessages = list()
+
+ def setCallback(self, callback):
+ self.callback = callback
+
+ def simulateInput(self, control, channel):
+ if self.callback is not None:
+ self.callback(control, channel)
+
+ def getLatestMessage(self):
+ return self.sentMessages[-1]
+
+@pytest.fixture
+def soloTool(mockPlayer, testSongs):
+ st = SoloTool(player=mockPlayer)
+ for song in testSongs:
+ st.addSong(song)
+ return st
+
+@pytest.fixture
+def midiWrapperMock(soloTool):
+ return MidiWrapperMock()
+
+@pytest.fixture
+def uut(soloTool, midiWrapperMock):
+ uut = ActitionController(midiWrapperMock)
+ uut.setSoloTool(soloTool)
+ return uut
+
+def test_rewindMessage(uut, soloTool, mockPlayer, midiWrapperMock):
+ soloTool.song = 1
+ mockPlayer.position = 0.5
+
+ # Sending rewind goes back to the start of the song
+ midiWrapperMock.simulateInput(REWIND, CHANNEL)
+ assert mockPlayer.getPlaybackPosition() == 0.0
+
+ # Sending again does not change the song
+ assert soloTool.song == 1
+ midiWrapperMock.simulateInput(REWIND, CHANNEL)
+ assert soloTool.song == 1
+ assert mockPlayer.position == 0.0
+
+def test_setMessage(uut, soloTool, mockPlayer, midiWrapperMock):
+ callbackValue = None
+ callbackCalled = False
+
+ def callback(keyPoint):
+ nonlocal callbackCalled, callbackValue
+ callbackValue = keyPoint
+ callbackCalled = True
+
+ soloTool.registerKeyPointSelectionCallback(callback)
+
+ # Sending set sets the current position as the key point
+ assert soloTool.keyPoint == 0.0
+
+ mockPlayer.position = 0.3
+ midiWrapperMock.simulateInput(SET, CHANNEL)
+ assert soloTool.keyPoint == 0.3
+ assert callbackCalled
+ assert callbackValue == 0.3
+
+ # Sending it again does nothing
+ callbackCalled = False
+ midiWrapperMock.simulateInput(SET, CHANNEL)
+ assert soloTool.keyPoint == 0.3
+ assert not callbackCalled
+
+def test_jumpMessage(uut, soloTool, mockPlayer, midiWrapperMock):
+ soloTool.keyPoint = 0.5
+ mockPlayer.position = 0.0
+
+ # Sending jump sets the player position to the current key point
+ midiWrapperMock.simulateInput(JUMP, CHANNEL)
+ assert mockPlayer.position == 0.5
+
+ # Sending again does nothing
+ midiWrapperMock.simulateInput(JUMP, CHANNEL)
+ assert mockPlayer.position == 0.5
+
+def test_playMessage(uut, soloTool, mockPlayer, midiWrapperMock):
+ callbackValue = None
+ callbackCalled = False
+
+ def callback(state):
+ nonlocal callbackCalled, callbackValue
+ callbackValue = state
+ callbackCalled = True
+
+ soloTool.registerPlayingStateCallback(callback)
+
+ # Sending play starts playing
+ assert not mockPlayer.isPlaying()
+ midiWrapperMock.simulateInput(PLAY, CHANNEL)
+ assert mockPlayer.isPlaying()
+ assert callbackCalled
+ assert callbackValue == True
+
+ # Sending again stops playing
+ callbackCalled = False
+ midiWrapperMock.simulateInput(PLAY, CHANNEL)
+ assert not mockPlayer.isPlaying()
+ assert callbackCalled
+ assert callbackValue == False
diff --git a/solo-tool-project/test/midi_launchpad_mini_integrationtest.py b/solo-tool-project/test/midi_launchpad_mini_integrationtest.py
index 1a99cd4..6841f24 100644
--- a/solo-tool-project/test/midi_launchpad_mini_integrationtest.py
+++ b/solo-tool-project/test/midi_launchpad_mini_integrationtest.py
@@ -1,7 +1,7 @@
import pytest
from mido import Message
-from solo_tool.midi_controller_launchpad_mini import MidiController
+from solo_tool.midi_controller_launchpad_mini import LaunchpadMiniController
from fixtures import soloTool, mockPlayer, testSongs
LED_RED = 3
@@ -38,10 +38,10 @@ class MidiWrapperMock:
def disconnect(self):
self.connectedDevice = None
-
- def sendMessage(self, note, velocity, channel):
+
+ def sendNoteOn(self, note, velocity, channel):
self.sentMessages.append((note, velocity, channel))
-
+
def simulateInput(self, note, velocity=127, channel=0):
if self.callback is not None:
msg = Message("note_on", note=note, velocity=velocity, channel=channel)
@@ -56,7 +56,7 @@ def midiWrapperMock():
@pytest.fixture
def uut(soloTool, midiWrapperMock):
- return MidiController(soloTool, midiWrapperMock)
+ return LaunchpadMiniController(soloTool, midiWrapperMock)
def test_startAndPauseButtons(uut, midiWrapperMock, mockPlayer):
uut.connect()
@@ -65,11 +65,11 @@ def test_startAndPauseButtons(uut, midiWrapperMock, mockPlayer):
midiWrapperMock.simulateInput(playPauseButton)
assert mockPlayer.playing
- assert midiWrapperMock.getLatestMessage() == (playPauseButton, MidiController.LED_GREEN, 0)
+ assert midiWrapperMock.getLatestMessage() == (playPauseButton, LaunchpadMiniController.LED_GREEN, 0)
midiWrapperMock.simulateInput(playPauseButton)
assert not mockPlayer.playing
- assert midiWrapperMock.getLatestMessage() == (playPauseButton, MidiController.LED_YELLOW, 0)
+ assert midiWrapperMock.getLatestMessage() == (playPauseButton, LaunchpadMiniController.LED_YELLOW, 0)
def test_startPauseButtonLed(uut, midiWrapperMock, mockPlayer, soloTool):
uut.connect()
@@ -78,11 +78,11 @@ def test_startPauseButtonLed(uut, midiWrapperMock, mockPlayer, soloTool):
mockPlayer.playing = True
mockPlayer.simulatePlayingStateChanged()
- assert midiWrapperMock.getLatestMessage() == (playPauseButton, MidiController.LED_GREEN, 0)
+ assert midiWrapperMock.getLatestMessage() == (playPauseButton, LaunchpadMiniController.LED_GREEN, 0)
mockPlayer.playing = False
mockPlayer.simulatePlayingStateChanged()
- assert midiWrapperMock.getLatestMessage() == (playPauseButton, MidiController.LED_YELLOW, 0)
+ assert midiWrapperMock.getLatestMessage() == (playPauseButton, LaunchpadMiniController.LED_YELLOW, 0)
def test_jumpToKeyPositionButton(uut, midiWrapperMock, soloTool, mockPlayer, testSongs):
soloTool.addSong(testSongs[0])
diff --git a/solo-tool-project/test/player_mock.py b/solo-tool-project/test/player_mock.py
index e9e9ead..a234e80 100644
--- a/solo-tool-project/test/player_mock.py
+++ b/solo-tool-project/test/player_mock.py
@@ -30,9 +30,11 @@ class Player():
return self.rate
def setPlaybackPosition(self, position):
+ print(f"{self} Setting playback position to {position}")
self.position = position
def getPlaybackPosition(self):
+ print(f"{self} Getting playback position: {self.position}")
return self.position
def setPlaybackVolume(self, volume):