aboutsummaryrefslogtreecommitdiffstats
path: root/solo-tool-project
diff options
context:
space:
mode:
Diffstat (limited to 'solo-tool-project')
-rw-r--r--solo-tool-project/pyproject.toml23
-rw-r--r--solo-tool-project/src/solo_tool/__init__.py1
-rw-r--r--solo-tool-project/src/solo_tool/abcontroller.py82
-rw-r--r--solo-tool-project/src/solo_tool/midi_controller_launchpad_mini.py145
-rw-r--r--solo-tool-project/src/solo_tool/midi_wrapper_mido.py21
-rw-r--r--solo-tool-project/src/solo_tool/notifier.py29
-rw-r--r--solo-tool-project/src/solo_tool/player_vlc.py55
-rw-r--r--solo-tool-project/src/solo_tool/playlist.py40
-rw-r--r--solo-tool-project/src/solo_tool/session_manager.py41
-rw-r--r--solo-tool-project/src/solo_tool/solo_tool.py164
-rw-r--r--solo-tool-project/test/abcontroller_unittest.py272
-rw-r--r--solo-tool-project/test/midi_launchpad_mini_integrationtest.py387
-rw-r--r--solo-tool-project/test/notifier_unittest.py100
-rw-r--r--solo-tool-project/test/player_mock.py71
-rw-r--r--solo-tool-project/test/playlist_unittest.py148
-rw-r--r--solo-tool-project/test/session_manager_unittest.py163
-rw-r--r--solo-tool-project/test/solo_tool_integrationtest.py594
-rw-r--r--solo-tool-project/test/test.flacbin0 -> 31743252 bytes
-rw-r--r--solo-tool-project/test/test.mp3bin0 -> 5389533 bytes
-rw-r--r--solo-tool-project/test/test_session.json13
20 files changed, 2349 insertions, 0 deletions
diff --git a/solo-tool-project/pyproject.toml b/solo-tool-project/pyproject.toml
new file mode 100644
index 0000000..36d4891
--- /dev/null
+++ b/solo-tool-project/pyproject.toml
@@ -0,0 +1,23 @@
+[build-system]
+requires = ["setuptools"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "solo_tool"
+authors = [
+ { name = "Eddy Pedroni", email = "epedroni@pm.me" },
+]
+description = "A library for dissecting guitar solos"
+requires-python = ">=3.12"
+dependencies = [
+ "python-rtmidi",
+ "sip",
+ "mido",
+ "python-vlc"
+]
+dynamic = ["version"]
+
+[project.optional-dependencies]
+dev = [
+ "pytest"
+]
diff --git a/solo-tool-project/src/solo_tool/__init__.py b/solo-tool-project/src/solo_tool/__init__.py
new file mode 100644
index 0000000..e1e7777
--- /dev/null
+++ b/solo-tool-project/src/solo_tool/__init__.py
@@ -0,0 +1 @@
+from .solo_tool import SoloTool
diff --git a/solo-tool-project/src/solo_tool/abcontroller.py b/solo-tool-project/src/solo_tool/abcontroller.py
new file mode 100644
index 0000000..cec9fb2
--- /dev/null
+++ b/solo-tool-project/src/solo_tool/abcontroller.py
@@ -0,0 +1,82 @@
+from collections import namedtuple
+
+_AB = namedtuple("_AB", ["a", "b"])
+
+class ABController:
+ def __init__(self, enabled=True, callback=None):
+ self._setPositionCallback = callback
+ self._limits = {} # dictionary of all songs
+ self._songLimits = None # list of limits for selected song
+ self._currentLimits = _AB(0.0, 0.0) # a/b positions of active limit
+ self._loadedIndex = None
+ self._enabled = enabled
+
+ def _ensureSongExists(self, path):
+ if path not in self._limits:
+ self._limits[path] = []
+
+ def setCurrentSong(self, path):
+ self._ensureSongExists(path)
+ self._songLimits = self._limits[path]
+ self._loadedIndex = None
+
+ def storeLimits(self, aLimit, bLimit, song=None):
+ if song is not None:
+ self._ensureSongExists(song)
+ songLimits = self._limits[song]
+ else:
+ songLimits = self._songLimits
+
+ if songLimits is None:
+ return
+
+ ab = _AB(aLimit, bLimit)
+ songLimits.append(ab)
+
+ def loadLimits(self, index):
+ if not self._songLimits:
+ return
+
+ if index >= 0 and index < len(self._songLimits):
+ self._currentLimits = self._songLimits[index]
+ self._loadedIndex = index
+
+ def nextStoredAbLimits(self):
+ if self._loadedIndex is None:
+ nextIndex = 0
+ else:
+ nextIndex = self._loadedIndex + 1
+ self.loadLimits(nextIndex)
+
+ def previousStoredAbLimits(self):
+ if self._loadedIndex is None:
+ previousIndex = 0
+ else:
+ previousIndex = self._loadedIndex - 1
+ self.loadLimits(previousIndex)
+
+ def setLimits(self, aLimit, bLimit):
+ self._currentLimits = _AB(aLimit, bLimit)
+ self._loadedIndex = None
+
+ def positionChanged(self, position):
+ if position > self._currentLimits.b and self._setPositionCallback and self._enabled:
+ self._setPositionCallback(self._currentLimits.a)
+
+ def setEnable(self, enable):
+ self._enabled = enable
+
+ def isEnabled(self):
+ return self._enabled
+
+ def getStoredLimits(self, song):
+ return self._limits.get(song)
+
+ def getCurrentLimits(self):
+ return self._currentLimits
+
+ def getLoadedIndex(self):
+ return self._loadedIndex
+
+ def clear(self):
+ self.__init__(enabled=self._enabled, callback=self._setPositionCallback)
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
new file mode 100644
index 0000000..961127c
--- /dev/null
+++ b/solo-tool-project/src/solo_tool/midi_controller_launchpad_mini.py
@@ -0,0 +1,145 @@
+from .midi_wrapper_mido import MidiWrapper
+
+class MidiController:
+ DEVICE_NAME = "Launchpad Mini MIDI 1"
+ LIGHT_CONTROL_CHANNEL = 0
+ LED_GREEN = 124
+ LED_YELLOW = 126
+ LED_RED = 3
+ LED_OFF = 0
+ BUTTON_MATRIX = [[x for x in range(y * 16, y * 16 + 8)] for y in range(0,8)] # address as [row][col]
+
+ MIN_PLAYBACK_RATE = 0.5
+ MAX_PLAYBACK_RATE = 1.2
+ PLAYBACK_RATE_STEP = 0.1
+
+ MIN_PLAYBACK_VOLUME = 0.5
+ MAX_PLAYBACK_VOLUME = 1.2
+ PLAYBACK_VOLUME_STEP = 0.1
+
+ def __init__(self, soloTool, midiWrapperOverride=None):
+ self._soloTool = soloTool
+ if midiWrapperOverride is not None:
+ self._midiWrapper = midiWrapperOverride
+ else:
+ self._midiWrapper = MidiWrapper()
+
+ self._registerHandlers()
+ self._soloTool.registerPlayingStateCallback(self._updatePlayPauseButton)
+ self._soloTool.registerPlaybackVolumeCallback(self._updateVolumeRow)
+ self._soloTool.registerPlaybackRateCallback(self._updateRateRow)
+ self._soloTool.registerAbLimitEnabledCallback(self._updateToggleAbLimitEnableButton)
+
+ def _registerHandlers(self):
+ self._handlers = {
+ 96 : self._soloTool.stop,
+ 99 : self._soloTool.jumpToA,
+ 112 : self._playPause,
+ 101 : self._toggleAbLimitEnable,
+ 102 : self._soloTool.previousStoredAbLimits,
+ 103 : self._soloTool.nextStoredAbLimits,
+ 118 : self._soloTool.previousSong,
+ 119 : self._soloTool.nextSong
+ }
+
+ for i in range(0, 8):
+ volume = round(MidiController.MIN_PLAYBACK_VOLUME + MidiController.PLAYBACK_VOLUME_STEP * i, 1)
+ self._handlers[i] = self._createSetPlaybackVolumeCallback(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)
+
+ def connect(self):
+ self._midiWrapper.setCallback(self._callback)
+ self._midiWrapper.connect(MidiController.DEVICE_NAME)
+ self._initialiseButtonLEDs()
+
+ def _callback(self, msg):
+ if msg.type != "note_on" or msg.velocity < 127:
+ return
+
+ 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 _toggleAbLimitEnable(self):
+ self._soloTool.setAbLimitEnable(not self._soloTool.isAbLimitEnabled())
+
+ 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, 5, MidiController.LED_GREEN)
+ else:
+ self._setButtonLED(6, 5, 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))
+ lastColumnLit = t1 - t2 + 1
+ self._lightRowUntilColumn(0, lastColumnLit, MidiController.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))
+ lastColumnLit = t1 - t2 + 1
+ self._lightRowUntilColumn(1, lastColumnLit, MidiController.LED_YELLOW)
+
+ def _createSetPlaybackRateCallback(self, rate):
+ def f():
+ self._soloTool.setPlaybackRate(rate)
+ return f
+
+ def _createSetPlaybackVolumeCallback(self, volume):
+ def f():
+ self._soloTool.setPlaybackVolume(volume)
+ return f
+
+ def _setButtonLED(self, row, col, colour):
+ self._midiWrapper.sendMessage(MidiController.BUTTON_MATRIX[row][col], colour, MidiController.LIGHT_CONTROL_CHANNEL)
+
+ def _lightRowUntilColumn(self, row, column, litColour):
+ colours = [litColour] * column + [MidiController.LED_OFF] * (8 - column)
+ for col in range(0, 8):
+ self._setButtonLED(row, col, colours[col])
+
+ def _allLEDsOff(self):
+ for row in range(0, 8):
+ for col in range(0, 8):
+ self._setButtonLED(row, col, MidiController.LED_OFF)
+
+ def _initialiseButtonLEDs(self):
+ self._allLEDsOff()
+
+ # volume buttons
+ self._updateVolumeRow(self._soloTool.getPlaybackVolume())
+
+ # playback rate buttons
+ self._updateRateRow(self._soloTool.getPlaybackRate())
+
+ # playback control
+ self._setButtonLED(6, 0, MidiController.LED_RED)
+ self._updatePlayPauseButton(self._soloTool.isPlaying())
+
+ # AB repeat toggle
+ self._updateToggleAbLimitEnableButton(self._soloTool.isAbLimitEnabled())
+
+ # AB control
+ self._setButtonLED(6, 3, MidiController.LED_YELLOW)
+ self._setButtonLED(6, 6, MidiController.LED_RED)
+ self._setButtonLED(6, 7, MidiController.LED_GREEN)
+
+ # Song control
+ self._setButtonLED(7, 6, MidiController.LED_RED)
+ self._setButtonLED(7, 7, MidiController.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
new file mode 100644
index 0000000..bf3aa85
--- /dev/null
+++ b/solo-tool-project/src/solo_tool/midi_wrapper_mido.py
@@ -0,0 +1,21 @@
+import mido
+
+class MidiWrapper:
+ def __init__(self):
+ self._inPort = None
+ self._outPort = None
+ self._callback = None
+
+ def setCallback(self, callback):
+ self._callback = callback
+
+ def connect(self, deviceName):
+ self._inPort = mido.open_input(deviceName)
+ self._inPort.callback = self._callback
+ self._outPort = mido.open_output(deviceName)
+
+ 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/src/solo_tool/notifier.py b/solo-tool-project/src/solo_tool/notifier.py
new file mode 100644
index 0000000..9f445b6
--- /dev/null
+++ b/solo-tool-project/src/solo_tool/notifier.py
@@ -0,0 +1,29 @@
+class Notifier:
+ PLAYING_STATE_EVENT = 0
+ PLAYBACK_VOLUME_EVENT = 1
+ PLAYBACK_RATE_EVENT = 2
+ CURRENT_SONG_EVENT = 3
+ CURRENT_AB_EVENT = 4
+ AB_LIMIT_ENABLED_EVENT = 5
+
+ def __init__(self, player):
+ self._callbacks = dict()
+ self._player = player
+ self._player.setPlayingStateChangedCallback(self._playingStateChangedCallback)
+ self._player.setPlaybackVolumeChangedCallback(self._playbackVolumeChangedCallback)
+
+ def registerCallback(self, event, callback):
+ if event not in self._callbacks:
+ self._callbacks[event] = list()
+ self._callbacks[event].append(callback)
+
+ def notify(self, event, value):
+ for callback in self._callbacks.get(event, list()):
+ callback(value)
+
+ def _playingStateChangedCallback(self, *args):
+ self.notify(Notifier.PLAYING_STATE_EVENT, self._player.isPlaying())
+
+ def _playbackVolumeChangedCallback(self, *args):
+ self.notify(Notifier.PLAYBACK_VOLUME_EVENT, self._player.getPlaybackVolume())
+
diff --git a/solo-tool-project/src/solo_tool/player_vlc.py b/solo-tool-project/src/solo_tool/player_vlc.py
new file mode 100644
index 0000000..283102e
--- /dev/null
+++ b/solo-tool-project/src/solo_tool/player_vlc.py
@@ -0,0 +1,55 @@
+import vlc
+
+class Player:
+ def __init__(self):
+ self._player = vlc.MediaPlayer()
+
+ def play(self):
+ self._player.play()
+
+ def stop(self):
+ self._player.stop()
+
+ def pause(self):
+ self._player.pause()
+
+ def isPlaying(self):
+ playing = self._player.is_playing() == 1
+ return playing
+
+ def setPlaybackRate(self, rate):
+ self._player.set_rate(rate)
+
+ def getPlaybackRate(self):
+ return self._player.get_rate()
+
+ def setPlaybackPosition(self, position):
+ self._player.set_position(position)
+
+ def getPlaybackPosition(self):
+ return self._player.get_position()
+
+ def setPlaybackVolume(self, volume):
+ self._player.audio_set_volume(int(volume * 100))
+
+ def getPlaybackVolume(self):
+ return self._player.audio_get_volume() / 100.0
+
+ def setCurrentSong(self, path):
+ self._player.stop()
+ media = vlc.Media(path)
+ self._player.set_media(media)
+
+ def setPlayingStateChangedCallback(self, callback):
+ events = [
+ vlc.EventType.MediaPlayerStopped,
+ vlc.EventType.MediaPlayerPlaying,
+ vlc.EventType.MediaPlayerPaused
+ ]
+ manager = self._player.event_manager()
+ for e in events:
+ manager.event_attach(e, callback)
+
+ def setPlaybackVolumeChangedCallback(self, callback):
+ manager = self._player.event_manager()
+ manager.event_attach(vlc.EventType.MediaPlayerAudioVolume, callback)
diff --git a/solo-tool-project/src/solo_tool/playlist.py b/solo-tool-project/src/solo_tool/playlist.py
new file mode 100644
index 0000000..bbfd8f5
--- /dev/null
+++ b/solo-tool-project/src/solo_tool/playlist.py
@@ -0,0 +1,40 @@
+class Playlist:
+ def __init__(self, callback):
+ self._songList = list()
+ self._currentSong = None
+ self._setSongCallback = callback
+
+ def addSong(self, path):
+ self._songList.append(path)
+
+ def setCurrentSong(self, index):
+ if index >= 0 and index < len(self._songList):
+ self._currentSong = index
+ self._setSongCallback(self._songList[index])
+
+ def getCurrentSong(self):
+ index = self._currentSong
+ return self._songList[index] if index is not None else None
+
+ def getCurrentSongIndex(self):
+ return self._currentSong
+
+ def getSongs(self):
+ return self._songList
+
+ def clear(self):
+ self.__init__(self._setSongCallback)
+
+ def nextSong(self):
+ if self._currentSong is None:
+ nextSong = 0
+ else:
+ nextSong = self._currentSong + 1
+ self.setCurrentSong(nextSong)
+
+ def previousSong(self):
+ if self._currentSong is None:
+ prevSong = 0
+ else:
+ prevSong = self._currentSong - 1
+ self.setCurrentSong(prevSong)
diff --git a/solo-tool-project/src/solo_tool/session_manager.py b/solo-tool-project/src/solo_tool/session_manager.py
new file mode 100644
index 0000000..718e864
--- /dev/null
+++ b/solo-tool-project/src/solo_tool/session_manager.py
@@ -0,0 +1,41 @@
+import json
+
+class SessionManager:
+ def __init__(self, playlist, abController):
+ self._playlist = playlist
+ self._abController = abController
+
+ def addSong(self, path):
+ self._playlist.addSong(path)
+
+ def storeLimits(self, aLimit, bLimit):
+ self._abController.storeLimits(aLimit, bLimit)
+
+ def loadSession(self, file):
+ jsonStr = file.read()
+ session = json.loads(jsonStr)
+
+ self._playlist.clear()
+ self._abController.clear()
+
+ for entry in session:
+ songPath = entry["path"]
+ abLimits = entry["ab_limits"]
+ self._playlist.addSong(songPath)
+
+ if abLimits is not None:
+ for l in abLimits:
+ self._abController.storeLimits(l[0], l[1], songPath)
+
+ def saveSession(self, file):
+ songs = self._playlist.getSongs()
+ session = list()
+
+ for s in songs:
+ entry = {
+ "path": s,
+ "ab_limits" : self._abController.getStoredLimits(s)
+ }
+ session.append(entry)
+
+ file.write(json.dumps(session))
diff --git a/solo-tool-project/src/solo_tool/solo_tool.py b/solo-tool-project/src/solo_tool/solo_tool.py
new file mode 100644
index 0000000..211babf
--- /dev/null
+++ b/solo-tool-project/src/solo_tool/solo_tool.py
@@ -0,0 +1,164 @@
+import os
+
+from .playlist import Playlist
+from .abcontroller import ABController
+from .session_manager import SessionManager
+from .notifier import Notifier
+from .player_vlc import Player
+
+class SoloTool:
+ def __init__(self, playerOverride=None):
+ self._player = Player() if playerOverride is None else playerOverride
+ self._playlist = Playlist(self._playlistCallback)
+ self._abController = ABController(enabled=False, callback=self._abControllerCallback)
+ self._sessionManager = SessionManager(self._playlist, self._abController)
+ self._notifier = Notifier(self._player)
+
+ def _playlistCallback(self, path):
+ self._player.setCurrentSong(path)
+ self._abController.setCurrentSong(path)
+
+ def _abControllerCallback(self, position):
+ self._player.setPlaybackPosition(position)
+
+ def tick(self):
+ position = self._player.getPlaybackPosition()
+ self._abController.positionChanged(position)
+
+ def addSong(self, path):
+ if os.path.isfile(path):
+ self._sessionManager.addSong(path)
+
+ def setSong(self, index):
+ previous = self._playlist.getCurrentSongIndex()
+ self._playlist.setCurrentSong(index)
+ new = self._playlist.getCurrentSongIndex()
+ if previous != new:
+ self._notifier.notify(Notifier.CURRENT_SONG_EVENT, new)
+
+ def nextSong(self):
+ previous = self._playlist.getCurrentSongIndex()
+ self._playlist.nextSong()
+ new = self._playlist.getCurrentSongIndex()
+ if previous != new:
+ self._notifier.notify(Notifier.CURRENT_SONG_EVENT, new)
+
+ def previousSong(self):
+ previous = self._playlist.getCurrentSongIndex()
+ self._playlist.previousSong()
+ new = self._playlist.getCurrentSongIndex()
+ if previous != new:
+ self._notifier.notify(Notifier.CURRENT_SONG_EVENT, new)
+
+ def getSongs(self):
+ return self._playlist.getSongs()
+
+ def storeAbLimits(self, aLimit, bLimit):
+ self._abController.storeLimits(aLimit, bLimit)
+
+ def loadAbLimits(self, index):
+ previous = self._abController.getLoadedIndex()
+ self._abController.loadLimits(index)
+ new = self._abController.getLoadedIndex()
+ if previous != new:
+ self._notifier.notify(Notifier.CURRENT_AB_EVENT, new)
+
+ def setAbLimits(self, aLimit, bLimit):
+ self._abController.setLimits(aLimit, bLimit)
+
+ def getStoredAbLimits(self):
+ currentSong = self._playlist.getCurrentSong()
+ if currentSong is not None:
+ return self._abController.getStoredLimits(currentSong)
+ else:
+ return list()
+
+ def setAbLimitEnable(self, enable):
+ previous = self._abController.isEnabled()
+ self._abController.setEnable(enable)
+ new = self._abController.isEnabled()
+ if previous != new:
+ self._notifier.notify(Notifier.AB_LIMIT_ENABLED_EVENT, new)
+
+ def isAbLimitEnabled(self):
+ return self._abController.isEnabled()
+
+ def nextStoredAbLimits(self):
+ previous = self._abController.getLoadedIndex()
+ self._abController.nextStoredAbLimits()
+ new = self._abController.getLoadedIndex()
+ if previous != new:
+ self._notifier.notify(Notifier.CURRENT_AB_EVENT, new)
+
+ def previousStoredAbLimits(self):
+ previous = self._abController.getLoadedIndex()
+ self._abController.previousStoredAbLimits()
+ new = self._abController.getLoadedIndex()
+ if previous != new:
+ self._notifier.notify(Notifier.CURRENT_AB_EVENT, new)
+
+ def jumpToA(self):
+ a = self._abController.getCurrentLimits()[0]
+ # XXX assumes that player.setPlaybackPosition is thread-safe!
+ self._player.setPlaybackPosition(a)
+
+ def loadSession(self, path):
+ with open(path, "r") as f:
+ self._sessionManager.loadSession(f)
+
+ def saveSession(self, path):
+ with open(path, "w") as f:
+ self._sessionManager.saveSession(f)
+
+ def play(self):
+ self._player.play()
+
+ def pause(self):
+ self._player.pause()
+
+ def stop(self):
+ self._player.stop()
+
+ def isPlaying(self):
+ return self._player.isPlaying()
+
+ def setPlaybackRate(self, rate):
+ previous = self._player.getPlaybackRate()
+ self._player.setPlaybackRate(rate)
+ new = self._player.getPlaybackRate()
+ if previous != new:
+ self._notifier.notify(Notifier.PLAYBACK_RATE_EVENT, new)
+
+ def getPlaybackRate(self):
+ return self._player.getPlaybackRate()
+
+ def setPlaybackPosition(self, position):
+ self._player.setPlaybackPosition(position)
+
+ def getPlaybackPosition(self):
+ return self._player.getPlaybackPosition()
+
+ def setPlaybackVolume(self, volume):
+ self._player.setPlaybackVolume(volume)
+
+ def getPlaybackVolume(self):
+ return self._player.getPlaybackVolume()
+
+ def registerPlayingStateCallback(self, callback):
+ self._notifier.registerCallback(Notifier.PLAYING_STATE_EVENT, callback)
+
+ def registerPlaybackVolumeCallback(self, callback):
+ self._notifier.registerCallback(Notifier.PLAYBACK_VOLUME_EVENT, callback)
+
+ def registerPlaybackRateCallback(self, callback):
+ self._notifier.registerCallback(Notifier.PLAYBACK_RATE_EVENT, callback)
+
+ def registerCurrentSongCallback(self, callback):
+ self._notifier.registerCallback(Notifier.CURRENT_SONG_EVENT, callback)
+
+ def registerCurrentAbLimitsCallback(self, callback):
+ self._notifier.registerCallback(Notifier.CURRENT_AB_EVENT, callback)
+
+ def registerAbLimitEnabledCallback(self, callback):
+ self._notifier.registerCallback(Notifier.AB_LIMIT_ENABLED_EVENT, callback)
+
diff --git a/solo-tool-project/test/abcontroller_unittest.py b/solo-tool-project/test/abcontroller_unittest.py
new file mode 100644
index 0000000..d2b7d31
--- /dev/null
+++ b/solo-tool-project/test/abcontroller_unittest.py
@@ -0,0 +1,272 @@
+from solo_tool.abcontroller import ABController
+from collections import namedtuple
+
+TCase = namedtuple("TCase", ["currentPosition", "requestedPosition"])
+AB = namedtuple("AB", ["a", "b"])
+abLimits = AB(0.2, 0.4)
+
+def _checkLimits(uut, tests):
+ requestedPosition = None
+ def callback(newPosition):
+ nonlocal requestedPosition
+ requestedPosition = newPosition
+
+ originalCallback = uut._setPositionCallback
+ uut._setPositionCallback = callback
+
+ for t in tests:
+ uut.positionChanged(t.currentPosition)
+ assert requestedPosition == t.requestedPosition
+
+ uut._setPositionCallback = originalCallback
+
+def checkLimits(uut, aLimit, bLimit, fail=False):
+ tests = [
+ TCase(aLimit - 0.1, None),
+ TCase(aLimit, None),
+ TCase(bLimit - 0.1, None),
+ TCase(bLimit, None),
+ TCase(bLimit + 0.1, aLimit if not fail else None)
+ ]
+ _checkLimits(uut, tests)
+ if not fail:
+ assert uut.getCurrentLimits()[0] == aLimit
+ assert uut.getCurrentLimits()[1] == bLimit
+
+def checkDefaultLimits(uut):
+ tests = [
+ TCase(0.0, None),
+ TCase(0.1, 0.0),
+ TCase(0.5, 0.0)
+ ]
+ _checkLimits(uut, tests)
+
+def test_oneSetOfLimits():
+ song = "/path/to/song"
+
+ uut = ABController()
+ uut.setCurrentSong(song)
+ uut.storeLimits(abLimits.a, abLimits.b)
+ uut.loadLimits(0)
+ assert uut.getLoadedIndex() == 0
+
+ checkLimits(uut, abLimits.a, abLimits.b)
+ assert uut.getStoredLimits(song) == [abLimits]
+
+def test_multipleSetsOfLimits():
+ song = "/path/to/song"
+ abLimits = [
+ AB(0.2, 0.4),
+ AB(0.3, 0.5),
+ AB(0.0, 1.2)
+ ]
+
+ uut = ABController()
+ uut.setCurrentSong(song)
+ for l in abLimits:
+ uut.storeLimits(l.a, l.b)
+
+ for i, l in enumerate(abLimits):
+ uut.loadLimits(i)
+ assert uut.getLoadedIndex() == i
+ checkLimits(uut, l.a, l.b)
+
+ assert uut.getStoredLimits(song) == abLimits
+
+def test_multipleSongs():
+ songs = [
+ "/path/to/song",
+ "/path/to/another/song"
+ ]
+ abLimits = [
+ AB(0.2, 0.4),
+ AB(0.3, 0.5)
+ ]
+ uut = ABController()
+ for i, s in enumerate(songs):
+ uut.storeLimits(abLimits[i].a, abLimits[i].b, s)
+
+ for i, s in enumerate(songs):
+ uut.setCurrentSong(s)
+ uut.loadLimits(0)
+ assert uut.getLoadedIndex() == 0
+
+ checkLimits(uut, abLimits[i].a, abLimits[i].b)
+ assert uut.getStoredLimits(s) == [abLimits[i]]
+
+def test_disableAbRepeat():
+ song = "/path/to/song"
+
+ uut = ABController()
+ uut.setCurrentSong(song)
+ uut.storeLimits(abLimits.a, abLimits.b)
+ uut.loadLimits(0)
+ assert uut.getLoadedIndex() == 0
+
+ assert uut.isEnabled()
+
+ uut.setEnable(False)
+ checkLimits(uut, abLimits.a, abLimits.b, fail=True)
+ assert not uut.isEnabled()
+
+ uut.setEnable(True)
+ checkLimits(uut, abLimits.a, abLimits.b)
+ assert uut.isEnabled()
+
+def test_storeLimitsToSpecificSong():
+ song = "/path/to/song"
+
+ uut = ABController()
+ uut.storeLimits(abLimits.a, abLimits.b, song)
+ uut.setCurrentSong(song)
+ uut.loadLimits(0)
+ assert uut.getLoadedIndex() == 0
+
+ checkLimits(uut, abLimits.a, abLimits.b)
+
+def test_storeLimitsWithoutCurrentSong():
+ uut = ABController()
+ uut.storeLimits(abLimits.a, abLimits.b)
+ uut.loadLimits(0)
+ assert uut.getLoadedIndex() == None
+
+ checkDefaultLimits(uut)
+
+def test_storeLimitsToSongWithoutCurrentSong():
+ song = "/path/to/song"
+ uut = ABController()
+ uut.storeLimits(abLimits.a, abLimits.b, song)
+ uut.loadLimits(0)
+ assert uut.getLoadedIndex() == None
+
+ checkDefaultLimits(uut)
+
+ uut.setCurrentSong(song)
+
+ checkDefaultLimits(uut)
+
+ uut.loadLimits(0)
+ assert uut.getLoadedIndex() == 0
+
+ checkLimits(uut, abLimits.a, abLimits.b)
+
+def test_storeLimitsToCurrentSongButDoNotSetCurrentLimits():
+ song = "/path/to/song"
+ uut = ABController()
+ uut.setCurrentSong(song)
+ uut.storeLimits(abLimits.a, abLimits.b)
+ assert uut.getLoadedIndex() == None
+
+ checkDefaultLimits(uut)
+
+ uut.loadLimits(0)
+ assert uut.getLoadedIndex() == 0
+
+ checkLimits(uut, abLimits.a, abLimits.b)
+
+def test_getStoredLimitsOfInexistentSong():
+ song = "/path/to/song"
+ uut = ABController()
+ assert uut.getStoredLimits(song) == None
+
+def test_clearAbController():
+ songsWithLimits = [
+ ("/path/to/song", AB(0.2, 0.4)),
+ ("/path/to/another/song", AB(0.3, 0.5))
+ ]
+
+ uut = ABController()
+ for s in songsWithLimits:
+ uut.storeLimits(s[1].a, s[1].b, s[0])
+
+ for i, s in enumerate(songsWithLimits):
+ assert uut.getStoredLimits(s[0]) == [s[1]]
+
+ uut.clear()
+
+ for i, s in enumerate(songsWithLimits):
+ assert uut.getStoredLimits(s[0]) == None
+
+def test_setTemporaryLimits():
+ abLimits = [
+ AB(0.2, 0.4),
+ AB(0.3, 0.5),
+ AB(0.0, 1.2)
+ ]
+ uut = ABController()
+
+ for l in abLimits:
+ uut.setLimits(l.a, l.b)
+ assert uut.getLoadedIndex() == None
+ checkLimits(uut, l.a, l.b)
+
+def test_setTemporaryLimitsWithCurrentSong():
+ songLimits = AB(0.2, 0.4)
+ abLimits = [
+ AB(0.2, 0.4),
+ AB(0.3, 0.5),
+ AB(0.0, 1.2)
+ ]
+ song = "/path/to/song"
+ uut = ABController()
+ uut.setCurrentSong(song)
+ uut.storeLimits(songLimits.a, songLimits.b)
+ uut.loadLimits(0)
+ assert uut.getLoadedIndex() == 0
+
+ for l in abLimits:
+ uut.setLimits(l.a, l.b)
+ checkLimits(uut, l.a, l.b)
+
+def test_defaultBehaviour():
+ uut = ABController()
+ checkDefaultLimits(uut)
+
+def test_nextStoredLimit():
+ song = "/path/to/song"
+ abLimits = [
+ AB(0.2, 0.4),
+ AB(0.3, 0.5)
+ ]
+
+ uut = ABController()
+ uut.setCurrentSong(song)
+ for l in abLimits:
+ uut.storeLimits(l.a, l.b)
+
+ checkDefaultLimits(uut)
+
+ uut.nextStoredAbLimits()
+ checkLimits(uut, abLimits[0].a, abLimits[0].b)
+
+ uut.nextStoredAbLimits()
+ checkLimits(uut, abLimits[1].a, abLimits[1].b)
+
+ uut.nextStoredAbLimits()
+ checkLimits(uut, abLimits[1].a, abLimits[1].b)
+
+def test_previousStoredLimit():
+ song = "/path/to/song"
+ abLimits = [
+ AB(0.2, 0.4),
+ AB(0.3, 0.5)
+ ]
+
+ uut = ABController()
+ uut.setCurrentSong(song)
+ for l in abLimits:
+ uut.storeLimits(l.a, l.b)
+
+ checkDefaultLimits(uut)
+
+ uut.previousStoredAbLimits()
+ checkLimits(uut, abLimits[0].a, abLimits[0].b)
+
+ uut.previousStoredAbLimits()
+ checkLimits(uut, abLimits[0].a, abLimits[0].b)
+
+ uut.loadLimits(1)
+ checkLimits(uut, abLimits[1].a, abLimits[1].b)
+
+ uut.previousStoredAbLimits()
+ checkLimits(uut, abLimits[0].a, abLimits[0].b)
diff --git a/solo-tool-project/test/midi_launchpad_mini_integrationtest.py b/solo-tool-project/test/midi_launchpad_mini_integrationtest.py
new file mode 100644
index 0000000..8542aae
--- /dev/null
+++ b/solo-tool-project/test/midi_launchpad_mini_integrationtest.py
@@ -0,0 +1,387 @@
+import pytest
+
+from solo_tool.midi_controller_launchpad_mini import MidiController
+from solo_tool.solo_tool import SoloTool
+from player_mock import Player as PlayerMock
+
+LED_RED = 3
+LED_YELLOW = 126
+LED_GREEN = 124
+LED_OFF = 0
+
+nextSongButton = 119
+previousSongButton = 118
+playPauseButton = 112
+stopButton = 96
+nextLimitButton = 103
+previousLimitButton = 102
+abToggleButton = 101
+jumpToAButton = 99
+
+class MidiWrapperMock:
+ def __init__(self):
+ self.callback = None
+ self.connectedDevice = None
+ self.sentMessages = list()
+
+ def setCallback(self, callback):
+ self.callback = callback
+
+ def connect(self, deviceName):
+ self.connectedDevice = deviceName
+
+ def sendMessage(self, note, velocity, channel):
+ self.sentMessages.append((note, velocity, channel))
+
+ def simulateInput(self, note, velocity=127, channel=0):
+ if self.callback is not None:
+ from mido import Message
+ msg = Message("note_on", note=note, velocity=velocity, channel=channel)
+ self.callback(msg)
+
+ def getLatestMessage(self):
+ return self.sentMessages[-1]
+
+@pytest.fixture
+def playerMock():
+ return PlayerMock()
+
+@pytest.fixture
+def soloTool(playerMock):
+ return SoloTool(playerMock)
+
+@pytest.fixture
+def midiWrapperMock():
+ return MidiWrapperMock()
+
+@pytest.fixture
+def uut(soloTool, midiWrapperMock):
+ return MidiController(soloTool, midiWrapperMock)
+
+def test_connect(uut, midiWrapperMock):
+ expectedDevice = "Launchpad Mini MIDI 1"
+ uut.connect()
+
+ assert midiWrapperMock.connectedDevice == expectedDevice
+
+def test_startStopAndPauseButtons(uut, midiWrapperMock, playerMock):
+ uut.connect()
+
+ assert playerMock.state == PlayerMock.STOPPED
+
+ midiWrapperMock.simulateInput(playPauseButton)
+ assert playerMock.state == PlayerMock.PLAYING
+ assert midiWrapperMock.getLatestMessage() == (playPauseButton, MidiController.LED_GREEN, 0)
+
+ midiWrapperMock.simulateInput(stopButton)
+ assert playerMock.state == PlayerMock.STOPPED
+ assert midiWrapperMock.getLatestMessage() == (playPauseButton, MidiController.LED_YELLOW, 0)
+
+ midiWrapperMock.simulateInput(playPauseButton)
+ assert midiWrapperMock.getLatestMessage() == (playPauseButton, MidiController.LED_GREEN, 0)
+
+ midiWrapperMock.simulateInput(playPauseButton)
+ assert midiWrapperMock.getLatestMessage() == (playPauseButton, MidiController.LED_YELLOW, 0)
+ assert playerMock.state == PlayerMock.PAUSED
+
+ midiWrapperMock.simulateInput(playPauseButton)
+ assert midiWrapperMock.getLatestMessage() == (playPauseButton, MidiController.LED_GREEN, 0)
+ assert playerMock.state == PlayerMock.PLAYING
+
+ midiWrapperMock.simulateInput(playPauseButton)
+ assert midiWrapperMock.getLatestMessage() == (playPauseButton, MidiController.LED_YELLOW, 0)
+
+ midiWrapperMock.simulateInput(stopButton)
+ assert playerMock.state == PlayerMock.STOPPED
+
+def test_startPauseButtonLed(uut, midiWrapperMock, playerMock, soloTool):
+ uut.connect()
+
+ assert playerMock.state == PlayerMock.STOPPED
+
+ playerMock.state = PlayerMock.PLAYING
+ playerMock.simulatePlayingStateChanged()
+ assert midiWrapperMock.getLatestMessage() == (playPauseButton, MidiController.LED_GREEN, 0)
+
+ playerMock.state = PlayerMock.STOPPED
+ playerMock.simulatePlayingStateChanged()
+ assert midiWrapperMock.getLatestMessage() == (playPauseButton, MidiController.LED_YELLOW, 0)
+
+ playerMock.state = PlayerMock.PAUSED
+ playerMock.simulatePlayingStateChanged()
+ assert midiWrapperMock.getLatestMessage() == (playPauseButton, MidiController.LED_YELLOW, 0)
+
+ playerMock.state = PlayerMock.PLAYING
+ playerMock.simulatePlayingStateChanged()
+ assert midiWrapperMock.getLatestMessage() == (playPauseButton, MidiController.LED_GREEN, 0)
+
+def test_abToggleButton(uut, midiWrapperMock, soloTool):
+ uut.connect()
+
+ midiWrapperMock.simulateInput(abToggleButton)
+ assert soloTool.isAbLimitEnabled()
+ assert midiWrapperMock.getLatestMessage() == (abToggleButton, MidiController.LED_GREEN, 0)
+
+ midiWrapperMock.simulateInput(abToggleButton)
+ assert not soloTool.isAbLimitEnabled()
+ assert midiWrapperMock.getLatestMessage() == (abToggleButton, MidiController.LED_RED, 0)
+
+def test_abToggleButtonLed(uut, midiWrapperMock, soloTool):
+ uut.connect()
+
+ soloTool.setAbLimitEnable(True)
+ assert midiWrapperMock.getLatestMessage() == (abToggleButton, MidiController.LED_GREEN, 0)
+
+ soloTool.setAbLimitEnable(False)
+ assert midiWrapperMock.getLatestMessage() == (abToggleButton, MidiController.LED_RED, 0)
+
+def test_jumpToAButton(uut, midiWrapperMock, soloTool, playerMock):
+ ab = (0.5, 0.6)
+ uut.connect()
+
+ soloTool.setAbLimits(ab[0], ab[1])
+ assert playerMock.position == 0.0
+
+ midiWrapperMock.simulateInput(jumpToAButton)
+ assert playerMock.position == ab[0]
+
+def test_previousAndNextSongButtons(uut, midiWrapperMock, soloTool, playerMock):
+ songs = [
+ "test.flac",
+ "test.mp3"
+ ]
+ for s in songs:
+ soloTool.addSong(s)
+ uut.connect()
+
+ assert playerMock.currentSong == None
+ midiWrapperMock.simulateInput(nextSongButton)
+ assert playerMock.currentSong == songs[0]
+
+ midiWrapperMock.simulateInput(nextSongButton)
+ assert playerMock.currentSong == songs[1]
+
+ midiWrapperMock.simulateInput(previousSongButton)
+ assert playerMock.currentSong == songs[0]
+
+ midiWrapperMock.simulateInput(previousSongButton)
+ assert playerMock.currentSong == songs[0]
+
+def test_previousAndNextAbButtons(uut, midiWrapperMock, soloTool, playerMock):
+ song = "test.flac"
+ abLimits = [
+ [0.2, 0.4],
+ [0.1, 0.3]
+ ]
+
+ soloTool.addSong(song)
+ soloTool.setSong(0)
+ soloTool.setAbLimitEnable(True)
+
+ for ab in abLimits:
+ soloTool.storeAbLimits(ab[0], ab[1])
+
+ uut.connect()
+
+ def checkLimit(aLimit, bLimit):
+ playerMock.position = bLimit - 0.1
+ soloTool.tick()
+ assert playerMock.position == bLimit - 0.1
+
+ playerMock.position = bLimit + 0.1
+ soloTool.tick()
+ assert playerMock.position == aLimit
+
+ checkLimit(0.0, 0.0)
+
+ midiWrapperMock.simulateInput(nextLimitButton)
+ checkLimit(abLimits[0][0], abLimits[0][1])
+
+ midiWrapperMock.simulateInput(nextLimitButton)
+ checkLimit(abLimits[1][0], abLimits[1][1])
+
+ midiWrapperMock.simulateInput(nextLimitButton)
+ checkLimit(abLimits[1][0], abLimits[1][1])
+
+ midiWrapperMock.simulateInput(previousLimitButton)
+ checkLimit(abLimits[0][0], abLimits[0][1])
+
+ midiWrapperMock.simulateInput(previousLimitButton)
+ checkLimit(abLimits[0][0], abLimits[0][1])
+
+def test_playbackRateButtons(uut, midiWrapperMock, soloTool, playerMock):
+ playbackRateOptions = {
+ 16 : (0.5, [LED_YELLOW] * 1 + [LED_OFF] * 7),
+ 17 : (0.6, [LED_YELLOW] * 2 + [LED_OFF] * 6),
+ 18 : (0.7, [LED_YELLOW] * 3 + [LED_OFF] * 5),
+ 19 : (0.8, [LED_YELLOW] * 4 + [LED_OFF] * 4),
+ 20 : (0.9, [LED_YELLOW] * 5 + [LED_OFF] * 3),
+ 21 : (1.0, [LED_YELLOW] * 6 + [LED_OFF] * 2),
+ 22 : (1.1, [LED_YELLOW] * 7 + [LED_OFF] * 1),
+ 23 : (1.2, [LED_YELLOW] * 8)
+ }
+ uut.connect()
+ assert playerMock.rate == 1.0
+
+ for t, button in enumerate(playbackRateOptions):
+ midiWrapperMock.sentMessages.clear()
+
+ midiWrapperMock.simulateInput(button)
+ assert playerMock.rate == playbackRateOptions[button][0]
+
+ for i, colour in enumerate(playbackRateOptions[button][1]):
+ assert midiWrapperMock.sentMessages[i] == (16 + i, colour, 0)
+
+def test_playbackRateLeds(uut, midiWrapperMock, soloTool, playerMock):
+ playbackRateOptions = [
+ (0.00, [LED_OFF] * 8),
+ (0.49, [LED_OFF] * 8),
+
+ (0.50, [LED_YELLOW] * 1 + [LED_OFF] * 7),
+ (0.59, [LED_YELLOW] * 1 + [LED_OFF] * 7),
+
+ (0.60, [LED_YELLOW] * 2 + [LED_OFF] * 6),
+ (0.69, [LED_YELLOW] * 2 + [LED_OFF] * 6),
+
+ (0.70, [LED_YELLOW] * 3 + [LED_OFF] * 5),
+ (0.79, [LED_YELLOW] * 3 + [LED_OFF] * 5),
+
+ (0.80, [LED_YELLOW] * 4 + [LED_OFF] * 4),
+ (0.89, [LED_YELLOW] * 4 + [LED_OFF] * 4),
+
+ (0.90, [LED_YELLOW] * 5 + [LED_OFF] * 3),
+ (0.99, [LED_YELLOW] * 5 + [LED_OFF] * 3),
+
+ (1.00, [LED_YELLOW] * 6 + [LED_OFF] * 2),
+ (1.09, [LED_YELLOW] * 6 + [LED_OFF] * 2),
+
+ (1.10, [LED_YELLOW] * 7 + [LED_OFF] * 1),
+ (1.19, [LED_YELLOW] * 7 + [LED_OFF] * 1),
+
+ (1.2, [LED_YELLOW] * 8),
+ (1.5, [LED_YELLOW] * 8)
+ ]
+ uut.connect()
+ assert playerMock.rate == 1.0
+
+ for t, (rate, leds) in enumerate(playbackRateOptions):
+ midiWrapperMock.sentMessages.clear()
+
+ soloTool.setPlaybackRate(rate)
+ assert playerMock.rate == rate
+
+ for i, colour in enumerate(leds):
+ assert midiWrapperMock.sentMessages[i] == (16 + i, colour, 0)
+
+def test_playbackVolumeButtons(uut, midiWrapperMock, soloTool, playerMock):
+ playbackVolumeOptions = {
+ 0 : (0.5, [LED_GREEN] * 1 + [LED_OFF] * 7),
+ 1 : (0.6, [LED_GREEN] * 2 + [LED_OFF] * 6),
+ 2 : (0.7, [LED_GREEN] * 3 + [LED_OFF] * 5),
+ 3 : (0.8, [LED_GREEN] * 4 + [LED_OFF] * 4),
+ 4 : (0.9, [LED_GREEN] * 5 + [LED_OFF] * 3),
+ 5 : (1.0, [LED_GREEN] * 6 + [LED_OFF] * 2),
+ 6 : (1.1, [LED_GREEN] * 7 + [LED_OFF] * 1),
+ 7 : (1.2, [LED_GREEN] * 8)
+ }
+ uut.connect()
+ assert playerMock.volume == 1.0
+
+ for t, button in enumerate(playbackVolumeOptions):
+ midiWrapperMock.sentMessages.clear()
+
+ midiWrapperMock.simulateInput(button)
+ assert playerMock.volume == playbackVolumeOptions[button][0]
+
+ for i, colour in enumerate(playbackVolumeOptions[button][1]):
+ assert midiWrapperMock.sentMessages[i] == (i, colour, 0)
+
+def test_playbackVolumeLeds(uut, midiWrapperMock, soloTool, playerMock):
+ playbackVolumeOptions = [
+ (0.00, [LED_OFF] * 8),
+ (0.49, [LED_OFF] * 8),
+
+ (0.50, [LED_GREEN] * 1 + [LED_OFF] * 7),
+ (0.59, [LED_GREEN] * 1 + [LED_OFF] * 7),
+
+ (0.60, [LED_GREEN] * 2 + [LED_OFF] * 6),
+ (0.69, [LED_GREEN] * 2 + [LED_OFF] * 6),
+
+ (0.70, [LED_GREEN] * 3 + [LED_OFF] * 5),
+ (0.79, [LED_GREEN] * 3 + [LED_OFF] * 5),
+
+ (0.80, [LED_GREEN] * 4 + [LED_OFF] * 4),
+ (0.89, [LED_GREEN] * 4 + [LED_OFF] * 4),
+
+ (0.90, [LED_GREEN] * 5 + [LED_OFF] * 3),
+ (0.99, [LED_GREEN] * 5 + [LED_OFF] * 3),
+
+ (1.00, [LED_GREEN] * 6 + [LED_OFF] * 2),
+ (1.09, [LED_GREEN] * 6 + [LED_OFF] * 2),
+
+ (1.10, [LED_GREEN] * 7 + [LED_OFF] * 1),
+ (1.19, [LED_GREEN] * 7 + [LED_OFF] * 1),
+
+ (1.2, [LED_GREEN] * 8),
+ (1.5, [LED_GREEN] * 8)
+ ]
+ uut.connect()
+ assert playerMock.volume == 1.0
+
+ for t, (volume, leds) in enumerate(playbackVolumeOptions):
+ midiWrapperMock.sentMessages.clear()
+
+ soloTool.setPlaybackVolume(volume)
+ assert playerMock.volume == volume
+
+ for i, colour in enumerate(leds):
+ assert midiWrapperMock.sentMessages[i] == (i, colour, 0)
+
+def test_unassignedButton(uut, midiWrapperMock):
+ unassignedButton = 48
+ uut.connect()
+
+ # expect no crash
+ midiWrapperMock.simulateInput(unassignedButton)
+ # XXX would be better to assert that nothing changed in the solo tool
+
+def test_initializationMessages(uut, midiWrapperMock):
+ expectedMessages = set(
+ [(int(i / 8) * 16 + (i % 8), LED_OFF, 0) for i in range(0, 64)] + # clear all
+ [(i, LED_GREEN, 0) for i in range(0, 6)] + # volume row
+ [(i, LED_YELLOW, 0) for i in range(16, 22)] + # playback rate row
+ [
+ (stopButton, LED_RED, 0),
+ (playPauseButton, LED_YELLOW, 0),
+ (abToggleButton, LED_RED, 0),
+ (jumpToAButton, LED_YELLOW, 0),
+ (previousLimitButton, LED_RED, 0),
+ (nextLimitButton, LED_GREEN, 0),
+ (previousSongButton, LED_RED, 0),
+ (nextSongButton, LED_GREEN, 0)
+ ]
+ )
+
+ uut.connect()
+
+ sentMessagesSet = set(midiWrapperMock.sentMessages)
+ assert sentMessagesSet == expectedMessages
+
+def test_playingFeedbackWhenChangingSong(uut, midiWrapperMock, soloTool, playerMock):
+ songs = [
+ "test.flac",
+ "test.mp3"
+ ]
+ for s in songs:
+ soloTool.addSong(s)
+ uut.connect()
+
+ soloTool.setSong(0)
+ soloTool.play()
+ assert playerMock.state == PlayerMock.PLAYING
+ assert midiWrapperMock.getLatestMessage() == (playPauseButton, LED_GREEN, 0)
+
+ soloTool.nextSong()
+ assert playerMock.state == PlayerMock.STOPPED
+ assert midiWrapperMock.getLatestMessage() == (playPauseButton, LED_YELLOW, 0)
+
diff --git a/solo-tool-project/test/notifier_unittest.py b/solo-tool-project/test/notifier_unittest.py
new file mode 100644
index 0000000..8a6e988
--- /dev/null
+++ b/solo-tool-project/test/notifier_unittest.py
@@ -0,0 +1,100 @@
+import pytest
+
+from solo_tool.notifier import Notifier
+from player_mock import Player
+
+@pytest.fixture
+def mockPlayer():
+ return Player()
+
+@pytest.fixture
+def uut(mockPlayer):
+ return Notifier(mockPlayer)
+
+def test_allEvents(uut):
+ def checkEvent(uut, event):
+ callbacks = 2
+ calledFlags = [False] * callbacks
+ values = [None] * callbacks
+
+ def createCallback(i):
+ def cb(value):
+ nonlocal calledFlags, values
+ calledFlags[i] = True
+ values[i] = value
+
+ return cb
+
+ for i in range(0, callbacks):
+ uut.registerCallback(event, createCallback(i))
+
+ assert not any(calledFlags)
+ uut.notify(event, 123)
+ assert all(calledFlags)
+ assert values == [123] * callbacks
+
+ checkEvent(uut, Notifier.PLAYING_STATE_EVENT)
+ checkEvent(uut, Notifier.PLAYBACK_VOLUME_EVENT)
+ checkEvent(uut, Notifier.PLAYBACK_RATE_EVENT)
+ checkEvent(uut, Notifier.CURRENT_SONG_EVENT)
+ checkEvent(uut, Notifier.CURRENT_AB_EVENT)
+ checkEvent(uut, Notifier.AB_LIMIT_ENABLED_EVENT)
+
+def test_eventWithoutRegisteredCallbacks(uut):
+ uut.notify(Notifier.PLAYING_STATE_EVENT, 0)
+ # expect no crash
+
+def test_eventsWithMockPlayer(uut, mockPlayer):
+ def checkEvent(eventCode, simulateEvent, expectedValue):
+ called = False
+ receivedValue = None
+ def callback(value):
+ nonlocal called, receivedValue
+ called = True
+ receivedValue = value
+
+ uut.registerCallback(eventCode, callback)
+
+ assert not called
+ simulateEvent()
+ assert called
+ assert receivedValue == expectedValue
+
+ mockPlayer.state = 1
+ mockPlayer.volume = 75
+
+ checkEvent(Notifier.PLAYING_STATE_EVENT, mockPlayer.simulatePlayingStateChanged, True)
+ checkEvent(Notifier.PLAYBACK_VOLUME_EVENT, mockPlayer.simulatePlaybackVolumeChanged, 75)
+
+def test_singleEventNotification(uut):
+ playingStateCalled = False
+ def playingStateCallback(value):
+ nonlocal playingStateCalled
+ playingStateCalled = True
+
+ volumeCalled = False
+ def volumeCallback(value):
+ nonlocal volumeCalled
+ volumeCalled = True
+
+ uut.registerCallback(Notifier.PLAYING_STATE_EVENT, playingStateCallback)
+ uut.registerCallback(Notifier.PLAYBACK_VOLUME_EVENT, volumeCallback)
+
+ assert not playingStateCalled
+ assert not volumeCalled
+
+ uut.notify(Notifier.PLAYING_STATE_EVENT, 0)
+ assert playingStateCalled
+ assert not volumeCalled
+
+ playingStateCalled = False
+
+ uut.notify(Notifier.PLAYBACK_VOLUME_EVENT, 0)
+ assert not playingStateCalled
+ assert volumeCalled
+
+ volumeCalled = False
+
+ uut.notify(Notifier.PLAYBACK_RATE_EVENT, 0)
+ assert not playingStateCalled
+ assert not volumeCalled
diff --git a/solo-tool-project/test/player_mock.py b/solo-tool-project/test/player_mock.py
new file mode 100644
index 0000000..3162e0f
--- /dev/null
+++ b/solo-tool-project/test/player_mock.py
@@ -0,0 +1,71 @@
+class Player():
+ STOPPED = 0
+ PLAYING = 1
+ PAUSED = 2
+
+ def __init__(self):
+ self.state = Player.STOPPED
+ self.rate = 1.0
+ self.position = 0.0
+ self.volume = 1.0
+ self.currentSong = None
+ self.playingStateChangedCallback = None
+ self.playbackVolumeChangedCallback = None
+
+ def play(self):
+ previousState = self.state
+ self.state = Player.PLAYING
+ if previousState != Player.PLAYING:
+ self.playingStateChangedCallback()
+
+ def stop(self):
+ previousState = self.state
+ self.state = Player.STOPPED
+ if previousState != Player.STOPPED:
+ self.playingStateChangedCallback()
+
+ def pause(self):
+ previousState = self.state
+ self.state = Player.PAUSED
+ if previousState != Player.PAUSED:
+ self.playingStateChangedCallback()
+
+ def isPlaying(self):
+ return self.state == Player.PLAYING
+
+ def setPlaybackRate(self, rate):
+ self.rate = rate
+
+ def getPlaybackRate(self):
+ return self.rate
+
+ def setPlaybackPosition(self, position):
+ self.position = position
+
+ def getPlaybackPosition(self):
+ return self.position
+
+ def setPlaybackVolume(self, volume):
+ changed = self.volume != volume
+ self.volume = volume
+ if changed:
+ self.playbackVolumeChangedCallback()
+
+ def getPlaybackVolume(self):
+ return self.volume
+
+ def setCurrentSong(self, path):
+ self.stop()
+ self.currentSong = path
+
+ def setPlayingStateChangedCallback(self, callback):
+ self.playingStateChangedCallback = callback
+
+ def simulatePlayingStateChanged(self):
+ self.playingStateChangedCallback()
+
+ def setPlaybackVolumeChangedCallback(self, callback):
+ self.playbackVolumeChangedCallback = callback
+
+ def simulatePlaybackVolumeChanged(self):
+ self.playbackVolumeChangedCallback()
diff --git a/solo-tool-project/test/playlist_unittest.py b/solo-tool-project/test/playlist_unittest.py
new file mode 100644
index 0000000..842ce51
--- /dev/null
+++ b/solo-tool-project/test/playlist_unittest.py
@@ -0,0 +1,148 @@
+from solo_tool.playlist import Playlist
+
+def test_addAndSelectOneSong():
+ songAddedByUser = "/path/to/song"
+ songSetByCallback = None
+
+ def testCallback(song):
+ nonlocal songSetByCallback
+ songSetByCallback = song
+
+ uut = Playlist(testCallback)
+ uut.addSong(songAddedByUser)
+ uut.setCurrentSong(0)
+
+ assert songAddedByUser == songSetByCallback
+ assert uut.getCurrentSong() == songAddedByUser
+ assert uut.getCurrentSongIndex() == 0
+ assert uut.getSongs() == [songAddedByUser]
+
+def test_addTwoSongsAndSelectBoth():
+ songAddedByUser = ["/path/to/song", "/path/to/second/song"]
+ songSetByCallback = None
+
+ def testCallback(song):
+ nonlocal songSetByCallback
+ songSetByCallback = song
+
+ uut = Playlist(testCallback)
+ uut.addSong(songAddedByUser[0])
+ uut.addSong(songAddedByUser[1])
+ assert uut.getSongs() == songAddedByUser
+
+ uut.setCurrentSong(0)
+ assert songAddedByUser[0] == songSetByCallback
+ assert uut.getCurrentSong() == songAddedByUser[0]
+ assert uut.getCurrentSongIndex() == 0
+
+ uut.setCurrentSong(1)
+ assert songAddedByUser[1] == songSetByCallback
+ assert uut.getCurrentSong() == songAddedByUser[1]
+ assert uut.getCurrentSongIndex() == 1
+
+def test_firstAddedSongIsNotSelected():
+ songAddedByUser = "/path/to/song"
+ songSetByCallback = None
+
+ def testCallback(song):
+ nonlocal songSetByCallback
+ songSetByCallback = song
+
+ uut = Playlist(testCallback)
+ uut.addSong(songAddedByUser)
+
+ assert songSetByCallback == None
+ assert uut.getCurrentSong() == None
+ assert uut.getCurrentSongIndex() == None
+ assert uut.getSongs() == [songAddedByUser]
+
+def test_invalidSongSelection():
+ songAddedByUser = "/path/to/song"
+ songSetByCallback = None
+
+ def testCallback(song):
+ nonlocal songSetByCallback
+ songSetByCallback = song
+
+ uut = Playlist(testCallback)
+ assert songSetByCallback == None
+ assert uut.getCurrentSong() == None
+ assert uut.getCurrentSongIndex() == None
+
+ uut.setCurrentSong(10)
+ assert songSetByCallback == None
+ assert uut.getCurrentSong() == None
+ assert uut.getCurrentSongIndex() == None
+
+ uut.addSong(songAddedByUser)
+ uut.setCurrentSong(10)
+ assert songSetByCallback == None
+ assert uut.getCurrentSong() == None
+ assert uut.getCurrentSongIndex() == None
+ assert uut.getSongs() == [songAddedByUser]
+
+def test_clearPlaylist():
+ songAddedByUser = ["/path/to/song", "/path/to/second/song"]
+
+ def dummy(index):
+ pass
+
+ uut = Playlist(dummy)
+ for s in songAddedByUser:
+ uut.addSong(s)
+ uut.setCurrentSong(0)
+
+ assert uut.getSongs() == songAddedByUser
+ assert uut.getCurrentSong() == songAddedByUser[0]
+ assert uut.getCurrentSongIndex() == 0
+
+ uut.clear()
+
+ assert uut.getSongs() == []
+ assert uut.getCurrentSong() == None
+ assert uut.getCurrentSongIndex() == None
+
+def test_nextSong():
+ songAddedByUser = ["/path/to/song", "/path/to/second/song"]
+
+ uut = Playlist(lambda index: None)
+ for s in songAddedByUser:
+ uut.addSong(s)
+ assert uut.getCurrentSong() == None
+ assert uut.getCurrentSongIndex() == None
+
+ uut.nextSong()
+ assert uut.getCurrentSong() == songAddedByUser[0]
+ assert uut.getCurrentSongIndex() == 0
+
+ uut.nextSong()
+ assert uut.getCurrentSong() == songAddedByUser[1]
+ assert uut.getCurrentSongIndex() == 1
+
+ uut.nextSong()
+ assert uut.getCurrentSong() == songAddedByUser[1]
+ assert uut.getCurrentSongIndex() == 1
+
+def test_previousSong():
+ songAddedByUser = ["/path/to/song", "/path/to/second/song"]
+
+ uut = Playlist(lambda index: None)
+ for s in songAddedByUser:
+ uut.addSong(s)
+ assert uut.getCurrentSong() == None
+ assert uut.getCurrentSongIndex() == None
+
+ uut.previousSong()
+ assert uut.getCurrentSong() == songAddedByUser[0]
+ assert uut.getCurrentSongIndex() == 0
+
+ uut.previousSong()
+ assert uut.getCurrentSong() == songAddedByUser[0]
+ assert uut.getCurrentSongIndex() == 0
+
+ uut.setCurrentSong(1)
+ assert uut.getCurrentSong() == songAddedByUser[1]
+ assert uut.getCurrentSongIndex() == 1
+ uut.previousSong()
+ assert uut.getCurrentSong() == songAddedByUser[0]
+ assert uut.getCurrentSongIndex() == 0
diff --git a/solo-tool-project/test/session_manager_unittest.py b/solo-tool-project/test/session_manager_unittest.py
new file mode 100644
index 0000000..5468880
--- /dev/null
+++ b/solo-tool-project/test/session_manager_unittest.py
@@ -0,0 +1,163 @@
+from solo_tool.session_manager import SessionManager
+from json import loads, dumps
+
+testSession = [
+ {
+ "path" : "/path/to/another/song",
+ "ab_limits" : None
+ },
+ {
+ "path" : "/path/to/song",
+ "ab_limits" : [
+ [0.1, 0.2],
+ [0.3, 0.4]
+ ]
+ },
+ {
+ "path" : "/path/to/something",
+ "ab_limits" : [
+ [0.1, 0.2]
+ ]
+ }
+]
+
+class PlaylistMock:
+ def __init__(self):
+ self.lastAddedSong = None
+ self.songs = list()
+
+ def addSong(self, s):
+ self.songs.append(s)
+ self.lastAddedSong = s
+
+ def getSongs(self):
+ return self.songs
+
+ def clear(self):
+ self.__init__()
+
+class ABControllerMock:
+ def __init__(self):
+ self.limits = dict()
+
+ def storeLimits(self, aLimit, bLimit, song="current"):
+ if song not in self.limits:
+ self.limits[song] = list()
+ self.limits[song].append([aLimit, bLimit])
+
+ def getStoredLimits(self, song):
+ return self.limits.get(song)
+
+ def clear(self):
+ self.__init__()
+
+class MockFile:
+ def __init__(self, init=""):
+ self.contents = init
+
+ def open(self, *args):
+ pass
+
+ def write(self, s):
+ self.contents += s
+
+ def read(self):
+ return self.contents
+
+
+def test_addSongs():
+ songs = [
+ "/path/to/song",
+ "/path/to/another/song"
+ ]
+
+ playlistMock = PlaylistMock()
+ uut = SessionManager(playlistMock, None)
+
+ for s in songs:
+ uut.addSong(s)
+ assert playlistMock.lastAddedSong == s
+
+def test_addAbLimits():
+ abLimits = [
+ [0.1, 0.2],
+ [0.3, 0.4]
+ ]
+
+ abControllerMock = ABControllerMock()
+ uut = SessionManager(None, abControllerMock)
+
+ for i, ab in enumerate(abLimits):
+ uut.storeLimits(ab[0], ab[1])
+ assert abControllerMock.limits["current"][i] == ab
+
+def test_loadSession():
+ playlistMock = PlaylistMock()
+ abControllerMock = ABControllerMock()
+ uut = SessionManager(playlistMock, abControllerMock)
+
+ sessionFile = MockFile(dumps(testSession))
+ uut.loadSession(sessionFile)
+
+ for i, entry in enumerate(testSession):
+ expectedSong = entry["path"]
+ expectedLimits = entry["ab_limits"]
+ loadedSong = playlistMock.songs[i]
+ loadedLimits = abControllerMock.limits.get(expectedSong)
+
+ assert loadedSong == expectedSong
+ assert loadedLimits == expectedLimits
+
+def test_saveSession():
+ playlistMock = PlaylistMock()
+ abControllerMock = ABControllerMock()
+ uut = SessionManager(playlistMock, abControllerMock)
+
+ for i, entry in enumerate(testSession):
+ song = entry["path"]
+ playlistMock.addSong(song)
+
+ abLimits = entry["ab_limits"]
+ if abLimits is not None:
+ for l in abLimits:
+ abControllerMock.storeLimits(l[0], l[1], song)
+
+ sessionFile = MockFile()
+ uut.saveSession(sessionFile)
+
+ savedSession = loads(sessionFile.read())
+ assert savedSession == testSession
+
+def test_loadAndSaveEmptySession():
+ playlistMock = PlaylistMock()
+ abControllerMock = ABControllerMock()
+ uut = SessionManager(playlistMock, abControllerMock)
+
+ sessionFile = MockFile()
+
+ uut.saveSession(sessionFile)
+ assert loads(sessionFile.read()) == list()
+
+ uut.loadSession(sessionFile)
+
+ songs = playlistMock.getSongs()
+ assert songs == list()
+ for s in songs:
+ assert abControllerMock.getStoredLimits(s) == None
+
+def test_loadSessionNotAdditive():
+ playlistMock = PlaylistMock()
+ abControllerMock = ABControllerMock()
+ uut = SessionManager(playlistMock, abControllerMock)
+
+ sessionFile = MockFile(dumps(testSession))
+ uut.loadSession(sessionFile)
+ uut.loadSession(sessionFile)
+
+ songs = playlistMock.getSongs()
+ assert len(songs) == len(set(songs))
+ for s in songs:
+ abLimits = abControllerMock.getStoredLimits(s)
+ if abLimits is not None:
+ abLimitStr = [f"[{l[0]}, {l[1]}] " for l in abLimits]
+ assert len(abLimitStr) == len(set(abLimitStr))
diff --git a/solo-tool-project/test/solo_tool_integrationtest.py b/solo-tool-project/test/solo_tool_integrationtest.py
new file mode 100644
index 0000000..5903abf
--- /dev/null
+++ b/solo-tool-project/test/solo_tool_integrationtest.py
@@ -0,0 +1,594 @@
+import pathlib
+import shutil
+import pytest
+
+from solo_tool.solo_tool import SoloTool
+from player_mock import Player as MockPlayer
+
+@pytest.fixture
+def mockPlayer():
+ return MockPlayer()
+
+@pytest.fixture
+def uut(mockPlayer):
+ return SoloTool(mockPlayer)
+
+@pytest.fixture
+def prepared_tmp_path(tmp_path):
+ testFiles = [
+ "test.flac",
+ "test.mp3",
+ "test_session.json"
+ ]
+ for f in testFiles:
+ shutil.copy(pathlib.Path(f), tmp_path)
+
+ return tmp_path
+
+def checkLimit(uut, mockPlayer, aLimit, bLimit):
+ mockPlayer.position = bLimit - 0.1
+ uut.tick()
+ assert mockPlayer.position == bLimit - 0.1
+
+ mockPlayer.position = bLimit + 0.1
+ uut.tick()
+ assert mockPlayer.position == aLimit
+
+def test_playerControls(uut, mockPlayer):
+ assert mockPlayer.state == MockPlayer.STOPPED
+ assert uut.isPlaying() == False
+ uut.play()
+ assert mockPlayer.state == MockPlayer.PLAYING
+ assert uut.isPlaying() == True
+ uut.pause()
+ assert mockPlayer.state == MockPlayer.PAUSED
+ assert uut.isPlaying() == False
+ uut.stop()
+ assert mockPlayer.state == MockPlayer.STOPPED
+ assert uut.isPlaying() == False
+
+ assert mockPlayer.rate == 1.0
+ uut.setPlaybackRate(0.5)
+ assert mockPlayer.rate == 0.5
+
+ assert mockPlayer.position == 0.0
+ uut.setPlaybackPosition(0.5)
+ assert mockPlayer.position == 0.5
+
+ assert mockPlayer.volume == 1.0
+ uut.setPlaybackVolume(0.5)
+ assert mockPlayer.volume == 0.5
+
+def test_addAndSetSongs(uut, mockPlayer):
+ songs = [
+ "test.flac",
+ "test.mp3"
+ ]
+
+ for s in songs:
+ uut.addSong(s)
+ assert mockPlayer.currentSong == None
+
+ for i, s in enumerate(songs):
+ uut.setSong(i)
+ assert mockPlayer.currentSong == songs[i]
+
+def test_nextAndPreviousSong(uut, mockPlayer):
+ songs = [
+ "test.flac",
+ "test.mp3"
+ ]
+
+ for s in songs:
+ uut.addSong(s)
+ assert mockPlayer.currentSong == None
+
+ uut.nextSong()
+ assert mockPlayer.currentSong == songs[0]
+
+ uut.previousSong()
+ assert mockPlayer.currentSong == songs[0]
+
+ uut.nextSong()
+ assert mockPlayer.currentSong == songs[1]
+
+ uut.nextSong()
+ assert mockPlayer.currentSong == songs[1]
+
+def test_addAndSetAbLimits(uut, mockPlayer):
+ song = "test.flac"
+ abLimits = [
+ [0.2, 0.4],
+ [0.1, 0.3]
+ ]
+
+ uut.addSong(song)
+ uut.setSong(0)
+
+ for ab in abLimits:
+ uut.storeAbLimits(ab[0], ab[1])
+
+ mockPlayer.position = 0.0
+ uut.tick()
+ assert mockPlayer.position == 0.0
+
+ mockPlayer.position = 0.5
+ uut.tick()
+ assert mockPlayer.position == 0.5
+
+ uut.loadAbLimits(0)
+
+ uut.tick()
+ assert mockPlayer.position == 0.5
+
+ uut.setAbLimitEnable(True)
+
+ uut.tick()
+ assert mockPlayer.position == 0.2
+
+ uut.tick()
+ assert mockPlayer.position == 0.2
+
+ uut.loadAbLimits(1)
+ uut.tick()
+ assert mockPlayer.position == 0.2
+
+ mockPlayer.position = 0.8
+ uut.tick()
+ assert mockPlayer.position == 0.1
+
+def test_abLimitEnabledGetter(uut):
+ assert not uut.isAbLimitEnabled()
+
+ uut.setAbLimitEnable(True)
+ assert uut.isAbLimitEnabled()
+
+ uut.setAbLimitEnable(False)
+ assert not uut.isAbLimitEnabled()
+
+def test_multipleSongsAndAbLimits(uut, mockPlayer):
+ songs = [
+ "test.flac",
+ "test.mp3"
+ ]
+ abLimits = [
+ [0.2, 0.4],
+ [0.5, 0.7]
+ ]
+
+ for s in songs:
+ uut.addSong(s)
+
+ for i, l in enumerate(abLimits):
+ uut.setSong(i)
+ uut.storeAbLimits(l[0], l[1])
+
+ uut.setAbLimitEnable(True)
+
+ for i, l in enumerate(abLimits):
+ uut.setSong(i)
+ uut.loadAbLimits(0)
+
+ mockPlayer.position = l[0]
+ uut.tick()
+ assert mockPlayer.position == l[0]
+
+ mockPlayer.position = l[1] + 0.1
+ uut.tick()
+ assert mockPlayer.position == l[0]
+
+def test_storeAbLimitsWithoutSong(uut, mockPlayer):
+ song = "test.flac"
+ abLimit = [0.2, 0.4]
+ overflow = abLimit[1] + 0.1
+ default = 0.0
+ mockPlayer.position = overflow
+ uut.setAbLimitEnable(True)
+
+ uut.storeAbLimits(abLimit[0], abLimit[1])
+ uut.tick()
+ assert mockPlayer.position == default
+ mockPlayer.position = overflow
+
+ uut.loadAbLimits(0)
+ uut.tick()
+ assert mockPlayer.position == default
+ mockPlayer.position = overflow
+
+ uut.addSong(song)
+ uut.tick()
+ assert mockPlayer.position == default
+ mockPlayer.position = overflow
+
+ uut.loadAbLimits(0)
+ uut.tick()
+ assert mockPlayer.position == default
+ mockPlayer.position = overflow
+
+ uut.setSong(0)
+ uut.tick()
+ assert mockPlayer.position == default
+ mockPlayer.position = overflow
+
+ uut.loadAbLimits(0)
+ uut.tick()
+ assert mockPlayer.position == default
+ mockPlayer.position = overflow
+
+ uut.storeAbLimits(abLimit[0], abLimit[1])
+ uut.tick()
+ assert mockPlayer.position == default
+ mockPlayer.position = overflow
+
+ uut.loadAbLimits(0)
+ uut.tick()
+ assert mockPlayer.position == abLimit[0]
+
+def test_nextAndPreviousAbLimit(uut, mockPlayer):
+ song = "test.flac"
+ abLimits = [
+ [0.2, 0.4],
+ [0.1, 0.3]
+ ]
+
+ uut.addSong(song)
+ uut.setSong(0)
+ uut.setAbLimitEnable(True)
+
+ for ab in abLimits:
+ uut.storeAbLimits(ab[0], ab[1])
+
+ checkLimit(uut, mockPlayer, 0.0, 0.0) # default limits
+
+ uut.nextStoredAbLimits()
+ checkLimit(uut, mockPlayer, abLimits[0][0], abLimits[0][1])
+
+ uut.nextStoredAbLimits()
+ checkLimit(uut, mockPlayer, abLimits[1][0], abLimits[1][1])
+
+ uut.nextStoredAbLimits()
+ checkLimit(uut, mockPlayer, abLimits[1][0], abLimits[1][1])
+
+ uut.previousStoredAbLimits()
+ checkLimit(uut, mockPlayer, abLimits[0][0], abLimits[0][1])
+
+ uut.previousStoredAbLimits()
+ checkLimit(uut, mockPlayer, abLimits[0][0], abLimits[0][1])
+
+def test_abLimitsWhenChangingSongs(uut, mockPlayer):
+ songs = [
+ "test.flac",
+ "test.mp3"
+ ]
+ abLimits = [
+ [0.2, 0.4],
+ [0.1, 0.3],
+ [0.7, 0.8]
+ ]
+ uut.setAbLimitEnable(True)
+
+ for s in songs:
+ uut.addSong(s)
+
+ uut.setSong(0)
+ for ab in abLimits:
+ uut.storeAbLimits(ab[0], ab[1])
+
+ uut.setSong(1)
+ uut.storeAbLimits(abLimits[0][0], abLimits[0][1])
+
+ uut.setSong(0)
+ uut.loadAbLimits(len(abLimits) - 1)
+ checkLimit(uut, mockPlayer, abLimits[-1][0], abLimits[-1][1])
+
+ uut.setSong(1)
+ checkLimit(uut, mockPlayer, abLimits[-1][0], abLimits[-1][1])
+
+ uut.previousStoredAbLimits()
+ checkLimit(uut, mockPlayer, abLimits[0][0], abLimits[0][1])
+
+def test_loadAndSaveSession(prepared_tmp_path):
+ mockPlayer = MockPlayer()
+ uut = SoloTool(mockPlayer)
+
+ loadedSessionFile = prepared_tmp_path / "test_session.json"
+ savedSessionFile = prepared_tmp_path / "test_session_save.json"
+
+ uut.loadSession(loadedSessionFile)
+ uut.saveSession(savedSessionFile)
+
+ import json
+ with open(loadedSessionFile, "r") as f:
+ loadedSession = json.loads(f.read())
+
+ with open(savedSessionFile, "r") as f:
+ savedSession = json.loads(f.read())
+
+ assert loadedSession == savedSession
+
+def test_addInexistentFile(uut, mockPlayer):
+ song = "not/a/real/file"
+
+ uut.addSong(song)
+ uut.setSong(0)
+
+ assert mockPlayer.currentSong == None
+
+def test_getters(uut, mockPlayer):
+ song = "test.flac"
+ abLimit = [0.2, 0.4]
+
+ uut.addSong(song)
+ uut.setSong(0)
+ uut.storeAbLimits(abLimit[0], abLimit[1])
+
+ assert uut.getSongs() == [song]
+
+ limits = uut.getStoredAbLimits()
+ assert len(limits) == 1
+ assert limits[0][0] == abLimit[0]
+ assert limits[0][1] == abLimit[1]
+
+ mockPlayer.position = 0.8
+ assert uut.getPlaybackPosition() == 0.8
+
+ mockPlayer.volume = 0.8
+ assert uut.getPlaybackVolume() == 0.8
+
+ mockPlayer.rate = 0.5
+ assert uut.getPlaybackRate() == 0.5
+
+def test_setTemporaryLimits(uut, mockPlayer):
+ song = "test.flac"
+ abLimits = [
+ [0.2, 0.4],
+ [0.1, 0.4]
+ ]
+ overflow = 0.5
+
+ uut.setAbLimitEnable(True)
+ mockPlayer.position = overflow
+ uut.addSong(song)
+ uut.setSong(0)
+ uut.storeAbLimits(abLimits[0][0], abLimits[0][1])
+ uut.loadAbLimits(0)
+
+ uut.setAbLimits(abLimits[1][0], abLimits[1][1])
+ uut.tick()
+ assert mockPlayer.position == abLimits[1][0]
+
+def test_jumpToA(uut, mockPlayer):
+ abLimits = (0.2, 0.4)
+ initialPosition = 0.8
+
+ mockPlayer.position = initialPosition
+
+ uut.jumpToA()
+ assert mockPlayer.position == 0.0 # default AB controller A limit
+
+ uut.setAbLimits(abLimits[0], abLimits[1])
+ uut.jumpToA()
+ assert mockPlayer.position == abLimits[0]
+
+def test_playingStateNotification(uut, mockPlayer):
+ song = "test.flac"
+ uut.addSong(song)
+ uut.setSong(0)
+
+ called = False
+ receivedValue = None
+ def callback(value):
+ nonlocal called, receivedValue
+ called = True
+ receivedValue = value
+
+ uut.registerPlayingStateCallback(callback)
+
+ assert mockPlayer.state == MockPlayer.STOPPED
+ assert not called
+
+ uut.play()
+ assert called
+ assert receivedValue == True
+ called = False
+ uut.play()
+ assert not called
+
+ uut.pause()
+ assert called
+ assert receivedValue == False
+ called = False
+ uut.pause()
+ assert not called
+
+ uut.play()
+ assert called
+ assert receivedValue == True
+ called = False
+
+ uut.stop()
+ assert called
+ assert receivedValue == False
+ called = False
+ uut.stop()
+ assert not called
+
+def test_playbackVolumeNotification(uut, mockPlayer):
+ song = "test.flac"
+ uut.addSong(song)
+ uut.setSong(0)
+
+ called = False
+ receivedValue = None
+ def callback(value):
+ nonlocal called, receivedValue
+ called = True
+ receivedValue = value
+
+ uut.registerPlaybackVolumeCallback(callback)
+
+ assert not called
+
+ uut.setPlaybackVolume(0.3)
+ assert called
+ assert receivedValue == 0.3
+ called = False
+
+ uut.setPlaybackVolume(0.3)
+ assert not called
+
+def test_playbackRateNotification(uut, mockPlayer):
+ song = "test.flac"
+ uut.addSong(song)
+ uut.setSong(0)
+
+ called = False
+ receivedValue = None
+ def callback(value):
+ nonlocal called, receivedValue
+ called = True
+ receivedValue = value
+
+ uut.registerPlaybackRateCallback(callback)
+
+ assert not called
+
+ uut.setPlaybackRate(0.5)
+ assert called
+ assert receivedValue == 0.5
+ called = False
+
+ uut.setPlaybackRate(0.5)
+ assert not called
+
+def test_currentSongNotification(uut):
+ called = False
+ receivedValue = None
+ def callback(value):
+ nonlocal called, receivedValue
+ called = True
+ receivedValue = value
+
+ uut.registerCurrentSongCallback(callback)
+ assert not called
+
+ songs = [
+ "test.flac",
+ "test.mp3"
+ ]
+ uut.addSong(songs[0])
+ assert not called
+
+ uut.setSong(0)
+ assert called
+ assert receivedValue == 0
+ called = False
+
+ uut.addSong(songs[1])
+ assert not called
+
+ uut.setSong(0)
+ assert not called
+
+ uut.setSong(1)
+ assert called
+ assert receivedValue == 1
+ called = False
+
+ uut.previousSong()
+ assert called
+ assert receivedValue == 0
+ called = False
+
+ uut.previousSong()
+ assert not called
+
+ uut.nextSong()
+ assert called
+ assert receivedValue == 1
+ called = False
+
+ uut.nextSong()
+ assert not called
+
+def test_currentAbNotification(uut):
+ called = False
+ receivedValue = None
+ def callback(value):
+ nonlocal called, receivedValue
+ called = True
+ receivedValue = value
+
+ uut.registerCurrentAbLimitsCallback(callback)
+ assert not called
+
+ song = "test.flac"
+ uut.addSong(song)
+ uut.setSong(0)
+
+ abLimits = [
+ (0.2, 0.3),
+ (0.4, 0.5)
+ ]
+ uut.storeAbLimits(abLimits[0][0], abLimits[0][1])
+ assert not called
+ uut.storeAbLimits(abLimits[1][0], abLimits[1][1])
+ assert not called
+
+ uut.loadAbLimits(0)
+ assert called
+ assert receivedValue == 0
+ called = False
+
+ uut.loadAbLimits(0)
+ assert not called
+
+ uut.loadAbLimits(1)
+ assert called
+ assert receivedValue == 1
+ called = False
+
+ uut.previousStoredAbLimits()
+ assert called
+ assert receivedValue == 0
+ called = False
+
+ uut.previousStoredAbLimits()
+ assert not called
+
+ uut.nextStoredAbLimits()
+ assert called
+ assert receivedValue == 1
+ called = False
+
+ uut.nextStoredAbLimits()
+ assert not called
+
+def test_abLimitEnabledNotification(uut):
+ called = False
+ receivedValue = None
+ def callback(value):
+ nonlocal called, receivedValue
+ called = True
+ receivedValue = value
+
+ uut.registerAbLimitEnabledCallback(callback)
+ assert not called
+
+ uut.setAbLimitEnable(False)
+ assert not called
+ assert receivedValue is None
+
+ uut.setAbLimitEnable(True)
+ assert called
+ assert receivedValue == True
+ called = False
+ receivedValue = None
+
+ uut.setAbLimitEnable(True)
+ assert not called
+ assert receivedValue is None
+
+ uut.setAbLimitEnable(False)
+ assert called
+ assert receivedValue == False
diff --git a/solo-tool-project/test/test.flac b/solo-tool-project/test/test.flac
new file mode 100644
index 0000000..9164735
--- /dev/null
+++ b/solo-tool-project/test/test.flac
Binary files differ
diff --git a/solo-tool-project/test/test.mp3 b/solo-tool-project/test/test.mp3
new file mode 100644
index 0000000..3c353b7
--- /dev/null
+++ b/solo-tool-project/test/test.mp3
Binary files differ
diff --git a/solo-tool-project/test/test_session.json b/solo-tool-project/test/test_session.json
new file mode 100644
index 0000000..f48b792
--- /dev/null
+++ b/solo-tool-project/test/test_session.json
@@ -0,0 +1,13 @@
+[
+ {
+ "path" : "test.flac",
+ "ab_limits" : null
+ },
+ {
+ "path" : "test.mp3",
+ "ab_limits" : [
+ [0.1, 0.2],
+ [0.3, 0.4]
+ ]
+ }
+]