diff options
Diffstat (limited to 'solo-tool-project')
24 files changed, 915 insertions, 1524 deletions
diff --git a/solo-tool-project/pyproject.toml b/solo-tool-project/pyproject.toml index 36d4891..841ee46 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" + "python-mpv" ] -dynamic = ["version"] [project.optional-dependencies] dev = [ diff --git a/solo-tool-project/src/solo_tool/abcontroller.py b/solo-tool-project/src/solo_tool/abcontroller.py deleted file mode 100644 index cec9fb2..0000000 --- a/solo-tool-project/src/solo_tool/abcontroller.py +++ /dev/null @@ -1,82 +0,0 @@ -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/handlers.py b/solo-tool-project/src/solo_tool/handlers.py new file mode 100644 index 0000000..1820e86 --- /dev/null +++ b/solo-tool-project/src/solo_tool/handlers.py @@ -0,0 +1,84 @@ +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 songRelative(st: SoloTool, delta: int) -> Callable[[], None]: + def f(): + if st.song is None: + st.song = 0 + else: + st.song += delta + return f + +def restartOrPreviousSong(st: SoloTool, threshold: float) -> Callable[[], None]: + def f(): + if st.position < threshold and st.song > 0: + st.song -= 1 + else: + st.position = 0.0 + return f + +def songAbsolute(st: SoloTool, index: int, followUp: Callable[[], None]=None) -> Callable[[], None]: + def f(): + st.song = index + if followUp is not None: + followUp() + return f + +def seekRelative(st: SoloTool, delta: float) -> Callable[[], None]: + def f(): + st.position += delta + return f + +def seekAbsolute(st: SoloTool, delta: float) -> Callable[[], None]: + def f(): + st.position = delta + return f + +def positionToKeyPoint(st: SoloTool) -> Callable[[], None]: + def f(): + st.keyPoint = st.position + return f + +def keyPointAbsolute(st: SoloTool, kp: float) -> Callable[[], None]: + def f(): + st.keyPoint = kp + return f + +def keyPointRelative(st: SoloTool, delta: int) -> Callable[[], None]: + from bisect import bisect_right, bisect_left + def f(): + l = sorted(set(st.keyPoints + [st.keyPoint])) + if delta > 0: + pivot = bisect_right(l, st.keyPoint) - 1 + elif delta < 0: + pivot = bisect_left(l, st.keyPoint) + else: + return + new = max(min(pivot + delta, len(l) - 1), 0) + st.keyPoint = l[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 4fde8fc..625e2ef 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,5 +1,5 @@ from .midi_wrapper_mido import MidiWrapper -from .solo_tool_controller import SoloToolController +from . import handlers class MidiController: DEVICE_NAME = "Launchpad Mini MIDI 1" @@ -20,7 +20,6 @@ class MidiController: def __init__(self, soloTool, midiWrapperOverride=None): self._soloTool = soloTool - self._soloToolController = SoloToolController(soloTool) if midiWrapperOverride is not None: self._midiWrapper = midiWrapperOverride else: @@ -28,47 +27,41 @@ class MidiController: self._registerHandlers() self._soloTool.registerPlayingStateCallback(self._updatePlayPauseButton) - self._soloTool.registerPlaybackVolumeCallback(self._updateVolumeRow) - self._soloTool.registerPlaybackRateCallback(self._updateRateRow) - self._soloTool.registerAbLimitEnabledCallback(self._updateToggleAbLimitEnableButton) - - self._aLimit = 0.0 - self._bLimit = 0.0 + self._soloTool.registerVolumeCallback(self._updateVolumeRow) + self._soloTool.registerRateCallback(self._updateRateRow) def _registerHandlers(self): self._handlers = { - 96 : self._soloTool.stop, - 114 : self._soloTool.jumpToA, - 112 : self._playPause, - 98 : self._toggleAbLimitEnable, - 118 : self._soloTool.previousStoredAbLimits, - 119 : self._soloTool.nextStoredAbLimits, - 116 : self._setALimit, - 117 : self._setBLimit, - 48 : self._soloToolController.previousSong, - 49 : self._createSeekHandler(-0.25), - 50 : self._createSeekHandler(-0.05), - 51 : self._createSeekHandler(-0.01), - 52 : self._createSeekHandler(0.01), - 53 : self._createSeekHandler(0.05), - 54 : self._createSeekHandler(0.25), - 55 : self._soloToolController.nextSong, + 96 : handlers.seekAbsolute(self._soloTool, 0.0), + 114 : self._soloTool.jump, + 112 : handlers.playPause(self._soloTool), + 118 : handlers.keyPointRelative(self._soloTool, -1), + 119 : handlers.keyPointRelative(self._soloTool, 1), + 117 : handlers.positionToKeyPoint(self._soloTool), + 48 : handlers.songRelative(self._soloTool, -1), + 49 : handlers.seekRelative(self._soloTool, -0.25), + 50 : handlers.seekRelative(self._soloTool, -0.05), + 51 : handlers.seekRelative(self._soloTool, -0.01), + 52 : handlers.seekRelative(self._soloTool, 0.01), + 53 : handlers.seekRelative(self._soloTool, 0.05), + 54 : handlers.seekRelative(self._soloTool, 0.25), + 55 : handlers.songRelative(self._soloTool, 1), } 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): @@ -78,42 +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 _createSeekHandler(self, delta): - def f(): - newPosition = self._soloTool.getPlaybackPosition() + delta - newPosition = min(1.0, max(0.0, newPosition)) - self._soloTool.setPlaybackPosition(newPosition) - return f - - def _setALimit(self): - self._aLimit = self._soloTool.getPlaybackPosition() - self._soloTool.setAbLimits(self._aLimit, self._bLimit) - - def _setBLimit(self): - self._bLimit = self._soloTool.getPlaybackPosition() - self._soloTool.setAbLimits(self._aLimit, self._bLimit) - - 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, 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)) @@ -126,16 +89,6 @@ class MidiController: 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) @@ -144,32 +97,28 @@ 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.getPlaybackVolume()) + self._updateVolumeRow(self._soloTool.volume) # playback rate buttons - self._updateRateRow(self._soloTool.getPlaybackRate()) + self._updateRateRow(self._soloTool.rate) # playback control - self._setButtonLED(6, 0, MidiController.LED_RED) - self._updatePlayPauseButton(self._soloTool.isPlaying()) - - # AB repeat toggle - self._updateToggleAbLimitEnableButton(self._soloTool.isAbLimitEnabled()) + self._setButtonLED(6, 0, MidiController.LED_YELLOW) + 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) - self._setButtonLED(7, 4, MidiController.LED_YELLOW) self._setButtonLED(7, 5, MidiController.LED_YELLOW) # Song control diff --git a/solo-tool-project/src/solo_tool/notifier.py b/solo-tool-project/src/solo_tool/notifier.py index 9f445b6..5b3539c 100644 --- a/solo-tool-project/src/solo_tool/notifier.py +++ b/solo-tool-project/src/solo_tool/notifier.py @@ -3,8 +3,9 @@ class Notifier: PLAYBACK_VOLUME_EVENT = 1 PLAYBACK_RATE_EVENT = 2 CURRENT_SONG_EVENT = 3 - CURRENT_AB_EVENT = 4 - AB_LIMIT_ENABLED_EVENT = 5 + SONG_LIST_EVENT = 4 + CURRENT_KEY_POINT_EVENT = 5 + KEY_POINT_LIST_EVENT = 6 def __init__(self, player): self._callbacks = dict() diff --git a/solo-tool-project/src/solo_tool/player_mpv.py b/solo-tool-project/src/solo_tool/player_mpv.py new file mode 100644 index 0000000..ff7fd1a --- /dev/null +++ b/solo-tool-project/src/solo_tool/player_mpv.py @@ -0,0 +1,53 @@ +import mpv + +class Player: + def __init__(self): + self._player = mpv.MPV() + self._player.loop = "inf" + self._playingStateCallback = self._dummyCallback + self._volumeCallback = self._dummyCallback + self._player.observe_property("pause", lambda name, value: self._playingStateCallback()) + self._player.observe_property("volume", lambda name, value: self._volumeCallback()) + + def __del__(self): + self._player.close() + + def _dummyCallback(self): + pass + + def play(self): + self._player.pause = False + + def pause(self): + self._player.pause = True + + def isPlaying(self): + return not self._player.pause + + def setPlaybackRate(self, rate): + self._player.speed = rate + + def getPlaybackRate(self): + return self._player.speed + + def setPlaybackPosition(self, position): + self._player.percent_pos = int(position * 100) + + def getPlaybackPosition(self): + return float(self._player.percent_pos or 0.0) / 100.0 + + def setPlaybackVolume(self, volume): + self._player.volume = int(volume * 100) + + def getPlaybackVolume(self): + return float(self._player.volume) / 100.0 + + def setCurrentSong(self, path): + self.pause() + self._player.play(str(path)) + + def setPlayingStateChangedCallback(self, callback): + self._playingStateCallback = callback + + def setPlaybackVolumeChangedCallback(self, callback): + self._volumeCallback = callback diff --git a/solo-tool-project/src/solo_tool/player_vlc.py b/solo-tool-project/src/solo_tool/player_vlc.py deleted file mode 100644 index 283102e..0000000 --- a/solo-tool-project/src/solo_tool/player_vlc.py +++ /dev/null @@ -1,55 +0,0 @@ -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/session_manager.py b/solo-tool-project/src/solo_tool/session_manager.py index a4dabc0..cd5ebf7 100644 --- a/solo-tool-project/src/solo_tool/session_manager.py +++ b/solo-tool-project/src/solo_tool/session_manager.py @@ -1,29 +1,30 @@ import json +from . import SoloTool -def loadSession(file, songList, abController): - jsonStr = file.read() - session = json.loads(jsonStr) +def loadSession(file: str, songPool: str, player=None) -> SoloTool: + with open(file, "r") as f: + session = json.load(f) - songList.clear() - abController.clear() + st = SoloTool(songPool, player=player) - for entry in session: + for i, entry in enumerate(session): songPath = entry["path"] - abLimits = entry["ab_limits"] - songList.append(songPath) + keyPoints = entry["key_points"] - if abLimits is not None: - for l in abLimits: - abController.storeLimits(l[0], l[1], songPath) + st.addSong(songPath) + st._keyPoints[i] = keyPoints + + return st -def saveSession(file, songList, abController): - session = list() +def saveSession(soloTool: SoloTool, file: str) -> None: + session = [] - for s in songList: + for i, song in enumerate(soloTool.songs): entry = { - "path": s, - "ab_limits" : abController.getStoredLimits(s) + "path": song, + "key_points" : soloTool._keyPoints[i] } session.append(entry) - file.write(json.dumps(session)) + with open(file, "w") as f: + json.dump(session, f) diff --git a/solo-tool-project/src/solo_tool/solo_tool.py b/solo-tool-project/src/solo_tool/solo_tool.py index a4c7af8..92b5595 100644 --- a/solo-tool-project/src/solo_tool/solo_tool.py +++ b/solo-tool-project/src/solo_tool/solo_tool.py @@ -1,39 +1,56 @@ import os +from pathlib import Path -from .abcontroller import ABController -from .session_manager import loadSession, saveSession from .notifier import Notifier -from .player_vlc import Player +from .player_mpv import Player class SoloTool: - def __init__(self, playerOverride=None): - self._player = Player() if playerOverride is None else playerOverride - self._abController = ABController(enabled=False, callback=self._abControllerCallback) + def __init__(self, songPool: str, player=None): + self._songPool = Path(songPool) + self._player = Player() if player is None else player self._notifier = Notifier(self._player) - self._songList = [] + self._songs = [] self._song = None + self._keyPoints = [] + self._keyPoint = None + + def __del__(self): + del self._player def _updateSong(self, index): + previousSong = self._song self._song = index - path = self._songList[index] - self._player.setCurrentSong(path) - self._abController.setCurrentSong(path) + self._player.pause() + self._player.setCurrentSong(self._songPool / self._songs[index]) self._notifier.notify(Notifier.CURRENT_SONG_EVENT, index) - def _abControllerCallback(self, position): - self._player.setPlaybackPosition(position) + previousKp = self._keyPoint + self._keyPoint = self.keyPoints[0] if len(self.keyPoints) > 0 else 0.0 + if previousKp != self._keyPoint: + self._notifier.notify(Notifier.CURRENT_KEY_POINT_EVENT, self._keyPoint) - def tick(self): - position = self._player.getPlaybackPosition() - self._abController.positionChanged(position) + if previousSong is None or self._keyPoints[previousSong] != self._keyPoints[index]: + self._notifier.notify(Notifier.KEY_POINT_LIST_EVENT, self.keyPoints) - @property - def songList(self) -> list[str]: - return self._songList + @staticmethod + def _keyPointValid(kp: float) -> bool: + return kp is not None and kp >= 0.0 and kp < 1.0 - def addSong(self, path: str) -> None: - if os.path.isfile(path): - self._songList.append(path) + @property + def songs(self) -> list[str]: + return self._songs.copy() + + def addSong(self, fileName: str) -> None: + path = self._songPool / fileName + if not os.path.isfile(path): + raise FileNotFoundError(path) + if path in self._songs: + return + self._songs.append(fileName) + self._keyPoints.append([]) + self._notifier.notify(Notifier.SONG_LIST_EVENT, self.songs) + if self.song is None: + self.song = 0 @property def song(self) -> int: @@ -41,64 +58,34 @@ class SoloTool: @song.setter def song(self, new: int) -> None: - if new >= 0 and new < len(self._songList) and new != self._song: + if new is not None \ + and new >= 0 \ + and new < len(self._songs) \ + and new != self._song: self._updateSong(new) - 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): - if self._song is not None: - return self._abController.getStoredLimits(self.songList[self._song]) - 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: - loadSession(f, self._songList, self._abController) - - def saveSession(self, path): - with open(path, "w") as f: - saveSession(f, self._songList, self._abController) + @property + def keyPoints(self) -> list[float]: + if self._song is None: + return None + return self._keyPoints[self._song].copy() + + @keyPoints.setter + def keyPoints(self, new: list[float]) -> None: + if new is not None and self._song is not None: + sanitized = sorted(list(set([p for p in new if SoloTool._keyPointValid(p)]))) + self._keyPoints[self._song] = sanitized.copy() + self._notifier.notify(Notifier.KEY_POINT_LIST_EVENT, self.keyPoints) + + @property + def keyPoint(self) -> float: + return float(self._keyPoint) if self._keyPoint is not None else None + + @keyPoint.setter + def keyPoint(self, new: float) -> None: + if self._song is not None and SoloTool._keyPointValid(new) and new != self._keyPoint: + self._keyPoint = new + self._notifier.notify(Notifier.CURRENT_KEY_POINT_EVENT, new) def play(self): self._player.play() @@ -106,49 +93,60 @@ class SoloTool: def pause(self): self._player.pause() - def stop(self): - self._player.stop() - - def isPlaying(self): + @property + def playing(self) -> bool: 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 jump(self): + self._player.setPlaybackPosition(self._keyPoint) - def getPlaybackRate(self): + @property + def rate(self) -> float: return self._player.getPlaybackRate() - def setPlaybackPosition(self, position): - self._player.setPlaybackPosition(position) + @rate.setter + def rate(self, new: float) -> None: + if new is not None and new >= 0.0 and new != self._player.getPlaybackRate(): + self._player.setPlaybackRate(new) + self._notifier.notify(Notifier.PLAYBACK_RATE_EVENT, new) - def getPlaybackPosition(self): + @property + def volume(self) -> float: + return self._player.getPlaybackVolume() + + @volume.setter + def volume(self, new: float) -> None: + if new is not None and new >= 0.0 and new != self._player.getPlaybackVolume(): + self._player.setPlaybackVolume(new) + self._notifier.notify(Notifier.PLAYBACK_VOLUME_EVENT, new) + + @property + def position(self) -> float: return self._player.getPlaybackPosition() - def setPlaybackVolume(self, volume): - self._player.setPlaybackVolume(volume) + @position.setter + def position(self, new: float) -> None: + if new is not None and new != self._player.getPlaybackPosition(): + self._player.setPlaybackPosition(min(max(0.0, new), 1.0)) - def getPlaybackVolume(self): - return self._player.getPlaybackVolume() + def registerSongSelectionCallback(self, callback): + self._notifier.registerCallback(Notifier.CURRENT_SONG_EVENT, callback) + + def registerSongListCallback(self, callback): + self._notifier.registerCallback(Notifier.SONG_LIST_EVENT, callback) + + def registerKeyPointSelectionCallback(self, callback): + self._notifier.registerCallback(Notifier.CURRENT_KEY_POINT_EVENT, callback) + + def registerKeyPointListCallback(self, callback): + self._notifier.registerCallback(Notifier.KEY_POINT_LIST_EVENT, callback) def registerPlayingStateCallback(self, callback): self._notifier.registerCallback(Notifier.PLAYING_STATE_EVENT, callback) - def registerPlaybackVolumeCallback(self, callback): + def registerVolumeCallback(self, callback): self._notifier.registerCallback(Notifier.PLAYBACK_VOLUME_EVENT, callback) - def registerPlaybackRateCallback(self, callback): + def registerRateCallback(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/src/solo_tool/solo_tool_controller.py b/solo-tool-project/src/solo_tool/solo_tool_controller.py deleted file mode 100644 index 0529570..0000000 --- a/solo-tool-project/src/solo_tool/solo_tool_controller.py +++ /dev/null @@ -1,22 +0,0 @@ -import os - -from solo_tool.solo_tool import SoloTool - -class SoloToolController: - def __init__(self, soloTool: SoloTool): - self._soloTool = soloTool - - def nextSong(self): - current = self._soloTool.song - if current is None: - self._soloTool.song = 0 - else: - self._soloTool.song = current + 1 - - def previousSong(self): - current = self._soloTool.song - if current is None: - self._soloTool.song = 0 - else: - self._soloTool.song = current - 1 - diff --git a/solo-tool-project/test/abcontroller_unittest.py b/solo-tool-project/test/abcontroller_unittest.py deleted file mode 100644 index d2b7d31..0000000 --- a/solo-tool-project/test/abcontroller_unittest.py +++ /dev/null @@ -1,272 +0,0 @@ -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/fixtures.py b/solo-tool-project/test/fixtures.py new file mode 100644 index 0000000..f70901b --- /dev/null +++ b/solo-tool-project/test/fixtures.py @@ -0,0 +1,32 @@ +import pytest +from pathlib import Path +import os + +from solo_tool.solo_tool import SoloTool +from player_mock import Player as MockPlayer + +@pytest.fixture +def mockPlayer(): + return MockPlayer() + +@pytest.fixture +def songPool(tmp_path): + return tmp_path / "songs" + +@pytest.fixture +def soloTool(mockPlayer, songPool): + return SoloTool(songPool, player=mockPlayer) + +@pytest.fixture +def testSongs(songPool): + songs = [ + songPool / "test.flac", + songPool / "test.mp3", + songPool / "test.mp4" + ] + + os.mkdir(songPool) + for song in songs: + song.touch() + return songs + diff --git a/solo-tool-project/test/handlers_integrationtest.py b/solo-tool-project/test/handlers_integrationtest.py new file mode 100644 index 0000000..85050d6 --- /dev/null +++ b/solo-tool-project/test/handlers_integrationtest.py @@ -0,0 +1,32 @@ +import pytest + +from fixtures import soloTool, testSongs, mockPlayer, songPool + +from solo_tool.handlers import keyPointRelative + +testCases = [ + ([0.1, 0.3], 0.0, +1, 0.1, "Start +1"), + ([0.1, 0.3], 0.1, +1, 0.3, "First +1"), + ([0.1, 0.3], 0.2, +1, 0.3, "Between +1"), + ([0.1, 0.3], 0.3, +1, 0.3, "Second +1"), + ([0.1, 0.3], 0.4, +1, 0.4, "End +1"), + + ([0.1, 0.3], 0.0, -1, 0.0, "Start -1"), + ([0.1, 0.3], 0.1, -1, 0.1, "First -1"), + ([0.1, 0.3], 0.2, -1, 0.1, "Between -1"), + ([0.1, 0.3], 0.3, -1, 0.1, "Second -1"), + ([0.1, 0.3], 0.4, -1, 0.3, "End -1"), + + ([0.0, 0.3], 0.0, -1, 0.0, "0.0 -1"), +] + +@pytest.mark.parametrize("keyPoints,current,delta,expected,description", testCases) +def test_keyPointRelativeEdgeCases(soloTool, testSongs, keyPoints, current, delta, expected, description): + soloTool.addSong(testSongs[0]) + soloTool.keyPoints = keyPoints + soloTool.keyPoint = current + + handler = keyPointRelative(soloTool, delta) + handler() + + assert soloTool.keyPoint == expected, description diff --git a/solo-tool-project/test/midi_launchpad_mini_integrationtest.py b/solo-tool-project/test/midi_launchpad_mini_integrationtest.py index c0d2b47..be8ed88 100644 --- a/solo-tool-project/test/midi_launchpad_mini_integrationtest.py +++ b/solo-tool-project/test/midi_launchpad_mini_integrationtest.py @@ -2,8 +2,7 @@ import pytest from mido import Message from solo_tool.midi_controller_launchpad_mini import MidiController -from solo_tool.solo_tool import SoloTool -from player_mock import Player as PlayerMock +from fixtures import songPool, soloTool, mockPlayer, testSongs LED_RED = 3 LED_YELLOW = 126 @@ -20,14 +19,12 @@ rwd25PcButton = 49 previousSongButton = 48 playPauseButton = 112 -stopButton = 96 +jumpToStartButton = 96 -nextLimitButton = 119 -previousLimitButton = 118 -abToggleButton = 98 -jumpToAButton = 114 -setAButton = 116 -setBButton = 117 +nextKeyPositionButton = 119 +previousKeyPositionButton = 118 +setKeyPositionButton = 117 +jumpToKeyPositionButton = 114 class MidiWrapperMock: def __init__(self): @@ -54,14 +51,6 @@ class MidiWrapperMock: return self.sentMessages[-1] @pytest.fixture -def playerMock(): - return PlayerMock() - -@pytest.fixture -def soloTool(playerMock): - return SoloTool(playerMock) - -@pytest.fixture def midiWrapperMock(): return MidiWrapperMock() @@ -69,152 +58,89 @@ def midiWrapperMock(): def uut(soloTool, midiWrapperMock): return MidiController(soloTool, midiWrapperMock) -def test_startStopAndPauseButtons(uut, midiWrapperMock, playerMock): +def test_startAndPauseButtons(uut, midiWrapperMock, mockPlayer): uut.connect() - assert playerMock.state == PlayerMock.STOPPED + assert not mockPlayer.playing midiWrapperMock.simulateInput(playPauseButton) - assert playerMock.state == PlayerMock.PLAYING + assert mockPlayer.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 not mockPlayer.playing 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): +def test_startPauseButtonLed(uut, midiWrapperMock, mockPlayer, soloTool): uut.connect() - assert playerMock.state == PlayerMock.STOPPED + assert not mockPlayer.playing - playerMock.state = PlayerMock.PLAYING - playerMock.simulatePlayingStateChanged() + mockPlayer.playing = True + mockPlayer.simulatePlayingStateChanged() assert midiWrapperMock.getLatestMessage() == (playPauseButton, MidiController.LED_GREEN, 0) - playerMock.state = PlayerMock.STOPPED - playerMock.simulatePlayingStateChanged() + mockPlayer.playing = False + mockPlayer.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) +def test_jumpToKeyPositionButton(uut, midiWrapperMock, soloTool, mockPlayer, testSongs): + soloTool.addSong(testSongs[0]) uut.connect() - soloTool.setAbLimits(ab[0], ab[1]) - assert playerMock.position == 0.0 + soloTool.keyPoint = 0.5 + assert mockPlayer.position == 0.0 - midiWrapperMock.simulateInput(jumpToAButton) - assert playerMock.position == ab[0] + midiWrapperMock.simulateInput(jumpToKeyPositionButton) + assert mockPlayer.position == 0.5 -def test_previousAndNextSongButtons(uut, midiWrapperMock, soloTool, playerMock): - songs = [ - "test.flac", - "test.mp3" - ] - for s in songs: +# TODO implement +def test_jumpToStartButton(uut, midiWrapperMock, soloTool, mockPlayer): + pass + +def test_previousAndNextSongButtons(uut, midiWrapperMock, soloTool, mockPlayer, testSongs): + for s in testSongs: soloTool.addSong(s) uut.connect() - assert playerMock.currentSong == None - midiWrapperMock.simulateInput(nextSongButton) - assert playerMock.currentSong == songs[0] - + assert mockPlayer.currentSong == testSongs[0] midiWrapperMock.simulateInput(nextSongButton) - assert playerMock.currentSong == songs[1] + assert mockPlayer.currentSong == testSongs[1] - midiWrapperMock.simulateInput(previousSongButton) - assert playerMock.currentSong == songs[0] + for _ in testSongs: + midiWrapperMock.simulateInput(nextSongButton) + assert mockPlayer.currentSong == testSongs[-1] midiWrapperMock.simulateInput(previousSongButton) - assert playerMock.currentSong == songs[0] + assert mockPlayer.currentSong == testSongs[-2] -def test_previousAndNextAbButtons(uut, midiWrapperMock, soloTool, playerMock): - song = "test.flac" - abLimits = [ - [0.2, 0.4], - [0.1, 0.3] - ] + for _ in testSongs: + midiWrapperMock.simulateInput(previousSongButton) + assert mockPlayer.currentSong == testSongs[0] - soloTool.addSong(song) - soloTool.song = 0 - soloTool.setAbLimitEnable(True) +def test_previousAndNextKeyPositionButtons(uut, midiWrapperMock, soloTool, mockPlayer, testSongs): + keyPoints = [0.2, 0.1] - for ab in abLimits: - soloTool.storeAbLimits(ab[0], ab[1]) + soloTool.addSong(testSongs[0]) + soloTool.keyPoints = keyPoints 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) + assert soloTool.keyPoint == 0.0 - midiWrapperMock.simulateInput(nextLimitButton) - checkLimit(abLimits[0][0], abLimits[0][1]) + midiWrapperMock.simulateInput(nextKeyPositionButton) + assert soloTool.keyPoint == 0.1 - midiWrapperMock.simulateInput(nextLimitButton) - checkLimit(abLimits[1][0], abLimits[1][1]) + midiWrapperMock.simulateInput(nextKeyPositionButton) + assert soloTool.keyPoint == 0.2 - midiWrapperMock.simulateInput(nextLimitButton) - checkLimit(abLimits[1][0], abLimits[1][1]) + midiWrapperMock.simulateInput(previousKeyPositionButton) + assert soloTool.keyPoint == 0.1 - midiWrapperMock.simulateInput(previousLimitButton) - checkLimit(abLimits[0][0], abLimits[0][1]) + midiWrapperMock.simulateInput(previousKeyPositionButton) + assert soloTool.keyPoint == 0.1 - midiWrapperMock.simulateInput(previousLimitButton) - checkLimit(abLimits[0][0], abLimits[0][1]) - -def test_playbackRateButtons(uut, midiWrapperMock, soloTool, playerMock): +def test_playbackRateButtons(uut, midiWrapperMock, soloTool, mockPlayer): playbackRateOptions = { 16 : (0.5, [LED_YELLOW] * 1 + [LED_OFF] * 7), 17 : (0.6, [LED_YELLOW] * 2 + [LED_OFF] * 6), @@ -226,18 +152,18 @@ def test_playbackRateButtons(uut, midiWrapperMock, soloTool, playerMock): 23 : (1.2, [LED_YELLOW] * 8) } uut.connect() - assert playerMock.rate == 1.0 + assert mockPlayer.rate == 1.0 for t, button in enumerate(playbackRateOptions): midiWrapperMock.sentMessages.clear() midiWrapperMock.simulateInput(button) - assert playerMock.rate == playbackRateOptions[button][0] + assert mockPlayer.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): +def test_playbackRateLeds(uut, midiWrapperMock, soloTool, mockPlayer): playbackRateOptions = [ (0.00, [LED_OFF] * 8), (0.49, [LED_OFF] * 8), @@ -267,18 +193,19 @@ def test_playbackRateLeds(uut, midiWrapperMock, soloTool, playerMock): (1.5, [LED_YELLOW] * 8) ] uut.connect() - assert playerMock.rate == 1.0 + assert mockPlayer.rate == 1.0 for t, (rate, leds) in enumerate(playbackRateOptions): + print(t) midiWrapperMock.sentMessages.clear() - soloTool.setPlaybackRate(rate) - assert playerMock.rate == rate + soloTool.rate = rate + assert mockPlayer.rate == rate for i, colour in enumerate(leds): assert midiWrapperMock.sentMessages[i] == (16 + i, colour, 0) -def test_playbackVolumeButtons(uut, midiWrapperMock, soloTool, playerMock): +def test_playbackVolumeButtons(uut, midiWrapperMock, soloTool, mockPlayer): playbackVolumeOptions = { 0 : (0.5, [LED_GREEN] * 1 + [LED_OFF] * 7), 1 : (0.6, [LED_GREEN] * 2 + [LED_OFF] * 6), @@ -290,18 +217,18 @@ def test_playbackVolumeButtons(uut, midiWrapperMock, soloTool, playerMock): 7 : (1.2, [LED_GREEN] * 8) } uut.connect() - assert playerMock.volume == 1.0 + assert mockPlayer.volume == 1.0 for t, button in enumerate(playbackVolumeOptions): midiWrapperMock.sentMessages.clear() midiWrapperMock.simulateInput(button) - assert playerMock.volume == playbackVolumeOptions[button][0] + assert mockPlayer.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): +def test_playbackVolumeLeds(uut, midiWrapperMock, soloTool, mockPlayer): playbackVolumeOptions = [ (0.00, [LED_OFF] * 8), (0.49, [LED_OFF] * 8), @@ -331,13 +258,13 @@ def test_playbackVolumeLeds(uut, midiWrapperMock, soloTool, playerMock): (1.5, [LED_GREEN] * 8) ] uut.connect() - assert playerMock.volume == 1.0 + assert mockPlayer.volume == 1.0 for t, (volume, leds) in enumerate(playbackVolumeOptions): midiWrapperMock.sentMessages.clear() - soloTool.setPlaybackVolume(volume) - assert playerMock.volume == volume + soloTool.volume = volume + assert mockPlayer.volume == volume for i, colour in enumerate(leds): assert midiWrapperMock.sentMessages[i] == (i, colour, 0) @@ -356,22 +283,20 @@ def test_connectDisconnect(uut, midiWrapperMock): [(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), - (setAButton, LED_YELLOW, 0), - (setBButton, LED_YELLOW, 0), - (previousSongButton, LED_RED, 0), - (rwd1PcButton, LED_RED, 0), - (rwd5PcButton, LED_RED, 0), - (rwd25PcButton, LED_RED, 0), - (nextSongButton, LED_GREEN, 0), - (fwd1PcButton, LED_GREEN, 0), - (fwd5PcButton, LED_GREEN, 0), - (fwd25PcButton, LED_GREEN, 0), + (jumpToStartButton, LED_YELLOW, 0), + (playPauseButton, LED_YELLOW, 0), + (jumpToKeyPositionButton, LED_YELLOW, 0), + (previousKeyPositionButton, LED_RED, 0), + (nextKeyPositionButton, LED_GREEN, 0), + (setKeyPositionButton, LED_YELLOW, 0), + (previousSongButton, LED_RED, 0), + (rwd1PcButton, LED_RED, 0), + (rwd5PcButton, LED_RED, 0), + (rwd25PcButton, LED_RED, 0), + (nextSongButton, LED_GREEN, 0), + (fwd1PcButton, LED_GREEN, 0), + (fwd5PcButton, LED_GREEN, 0), + (fwd25PcButton, LED_GREEN, 0), ]) teardownMessages = [(int(i / 8) * 16 + (i % 8), LED_OFF, 0) for i in range(0, 64)] # clear all @@ -388,93 +313,67 @@ def test_connectDisconnect(uut, midiWrapperMock): assert set(midiWrapperMock.sentMessages) == set(teardownMessages) -def test_playingFeedbackWhenChangingSong(uut, midiWrapperMock, soloTool, playerMock): - songs = [ - "test.flac", - "test.mp3" - ] - for s in songs: +def test_playingFeedbackWhenChangingSong(uut, midiWrapperMock, soloTool, mockPlayer, testSongs): + for s in testSongs: soloTool.addSong(s) uut.connect() - soloTool.song = 0 soloTool.play() - assert playerMock.state == PlayerMock.PLAYING + assert mockPlayer.playing assert midiWrapperMock.getLatestMessage() == (playPauseButton, LED_GREEN, 0) soloTool.song = 1 - assert playerMock.state == PlayerMock.STOPPED + assert not mockPlayer.playing assert midiWrapperMock.getLatestMessage() == (playPauseButton, LED_YELLOW, 0) -def test_setAbButtons(uut, midiWrapperMock, soloTool, playerMock): - song = "test.flac" - soloTool.addSong(song) - soloTool.song = 0 - soloTool.setAbLimitEnable(True) - abLimits = (0.6, 0.8) - soloTool.storeAbLimits(abLimits[0], abLimits[1]) +def test_setKeyPositionButton(uut, midiWrapperMock, soloTool, mockPlayer, testSongs): + soloTool.addSong(testSongs[0]) 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 - - # Set A limit - playerMock.position = 0.3 - midiWrapperMock.simulateInput(setAButton) - playerMock.position = 0.5 - midiWrapperMock.simulateInput(jumpToAButton) - - assert playerMock.position == 0.3 + mockPlayer.position = 0.3 + midiWrapperMock.simulateInput(setKeyPositionButton) + assert soloTool.keyPoint == 0.3 - # Set B limit - playerMock.position = 0.4 - midiWrapperMock.simulateInput(setBButton) - checkLimit(0.3, 0.4) + mockPlayer.position = 0.5 + midiWrapperMock.simulateInput(setKeyPositionButton) + assert soloTool.keyPoint == 0.5 - # Selecting preset overrides manually set limits - midiWrapperMock.simulateInput(nextLimitButton) - checkLimit(abLimits[0], abLimits[1]) + mockPlayer.position = 0.7 + midiWrapperMock.simulateInput(jumpToKeyPositionButton) + assert mockPlayer.position == 0.5 -def test_seekButtons(uut, midiWrapperMock, soloTool, playerMock): - song = "test.flac" - soloTool.addSong(song) - soloTool.song = 0 +def test_seekButtons(uut, midiWrapperMock, soloTool, mockPlayer, testSongs): + soloTool.addSong(testSongs[0]) uut.connect() - assert playerMock.position == 0.0 + assert mockPlayer.position == 0.0 midiWrapperMock.simulateInput(fwd25PcButton) - assert playerMock.position == 0.25 + assert mockPlayer.position == 0.25 midiWrapperMock.simulateInput(fwd5PcButton) - assert playerMock.position == 0.30 + assert mockPlayer.position == 0.30 midiWrapperMock.simulateInput(fwd1PcButton) - assert playerMock.position == 0.31 + assert mockPlayer.position == 0.31 midiWrapperMock.simulateInput(fwd25PcButton) midiWrapperMock.simulateInput(fwd25PcButton) midiWrapperMock.simulateInput(fwd25PcButton) - assert playerMock.position == 1.0 + assert mockPlayer.position == 1.0 midiWrapperMock.simulateInput(rwd25PcButton) - assert playerMock.position == 0.75 + assert mockPlayer.position == 0.75 midiWrapperMock.simulateInput(rwd5PcButton) - assert playerMock.position == 0.70 + assert mockPlayer.position == 0.70 midiWrapperMock.simulateInput(rwd1PcButton) - assert playerMock.position == 0.69 + assert mockPlayer.position == 0.69 midiWrapperMock.simulateInput(rwd25PcButton) midiWrapperMock.simulateInput(rwd25PcButton) midiWrapperMock.simulateInput(rwd25PcButton) - assert playerMock.position == 0.0 + assert mockPlayer.position == 0.0 diff --git a/solo-tool-project/test/notifier_unittest.py b/solo-tool-project/test/notifier_unittest.py index 8a6e988..5749149 100644 --- a/solo-tool-project/test/notifier_unittest.py +++ b/solo-tool-project/test/notifier_unittest.py @@ -37,8 +37,8 @@ def test_allEvents(uut): 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) + checkEvent(uut, Notifier.CURRENT_KEY_POINT_EVENT) + checkEvent(uut, Notifier.KEY_POINT_LIST_EVENT) def test_eventWithoutRegisteredCallbacks(uut): uut.notify(Notifier.PLAYING_STATE_EVENT, 0) @@ -60,7 +60,7 @@ def test_eventsWithMockPlayer(uut, mockPlayer): assert called assert receivedValue == expectedValue - mockPlayer.state = 1 + mockPlayer.playing = True mockPlayer.volume = 75 checkEvent(Notifier.PLAYING_STATE_EVENT, mockPlayer.simulatePlayingStateChanged, True) diff --git a/solo-tool-project/test/player_mock.py b/solo-tool-project/test/player_mock.py index 3162e0f..e9e9ead 100644 --- a/solo-tool-project/test/player_mock.py +++ b/solo-tool-project/test/player_mock.py @@ -1,10 +1,6 @@ class Player(): - STOPPED = 0 - PLAYING = 1 - PAUSED = 2 - def __init__(self): - self.state = Player.STOPPED + self.playing = False self.rate = 1.0 self.position = 0.0 self.volume = 1.0 @@ -13,25 +9,19 @@ class Player(): 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: + previousState = self.playing + self.playing = True + if previousState != self.playing: self.playingStateChangedCallback() def pause(self): - previousState = self.state - self.state = Player.PAUSED - if previousState != Player.PAUSED: + previousState = self.playing + self.playing = False + if previousState != self.playing: self.playingStateChangedCallback() def isPlaying(self): - return self.state == Player.PLAYING + return self.playing def setPlaybackRate(self, rate): self.rate = rate @@ -55,7 +45,6 @@ class Player(): return self.volume def setCurrentSong(self, path): - self.stop() self.currentSong = path def setPlayingStateChangedCallback(self, callback): diff --git a/solo-tool-project/test/session_manager_unittest.py b/solo-tool-project/test/session_manager_unittest.py index d89b82a..cff3b35 100644 --- a/solo-tool-project/test/session_manager_unittest.py +++ b/solo-tool-project/test/session_manager_unittest.py @@ -1,114 +1,65 @@ -from solo_tool.session_manager import loadSession, saveSession -from json import loads, dumps - import pytest +from json import loads +import os -testSession = [ - { - "path" : "/path/to/another/song", - "ab_limits" : None - }, +from solo_tool.session_manager import loadSession, saveSession +from fixtures import songPool, soloTool, mockPlayer, testSongs + +@pytest.fixture +def testSessionFile(tmp_path, testSongs): + contents = """[ { - "path" : "/path/to/song", - "ab_limits" : [ - [0.1, 0.2], - [0.3, 0.4] - ] + "path" : "test.flac", + "key_points" : [] }, { - "path" : "/path/to/something", - "ab_limits" : [ - [0.1, 0.2] - ] + "path" : "test.mp3", + "key_points" : [0.1, 0.3] } -] - -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 + basePath = tmp_path / "sessions" + sessionFile = basePath / "test-session.json" - def open(self, *args): - pass + os.mkdir(basePath) + with open(sessionFile, "w") as f: + f.write(contents) - def write(self, s): - self.contents += s + return sessionFile - def read(self): - return self.contents +def test_loadSession(songPool, testSessionFile, mockPlayer): + soloTool = loadSession(testSessionFile, songPool, player=mockPlayer) -@pytest.fixture -def playlistMock(): - return [] - -@pytest.fixture -def abControllerMock(): - return ABControllerMock() + assert soloTool.songs == ["test.flac", "test.mp3"] -def test_loadSession(playlistMock, abControllerMock): - sessionFile = MockFile(dumps(testSession)) - loadSession(sessionFile, playlistMock, abControllerMock) + soloTool.song = 0 + assert soloTool.keyPoints == [] - for i, entry in enumerate(testSession): - expectedSong = entry["path"] - expectedLimits = entry["ab_limits"] - loadedSong = playlistMock[i] - loadedLimits = abControllerMock.limits.get(expectedSong) + soloTool.song = 1 + assert soloTool.keyPoints == [0.1, 0.3] - assert loadedSong == expectedSong - assert loadedLimits == expectedLimits +def test_saveSession(soloTool, testSessionFile, tmp_path): + soloTool.addSong("test.flac") + soloTool.addSong("test.mp3") + soloTool.song = 1 + soloTool.keyPoints = [0.1, 0.3] -def test_saveSession(playlistMock, abControllerMock): - for i, entry in enumerate(testSession): - song = entry["path"] - playlistMock.append(song) + testFile = tmp_path / "test_session_saved.json" + saveSession(soloTool, testFile) - abLimits = entry["ab_limits"] - if abLimits is not None: - for l in abLimits: - abControllerMock.storeLimits(l[0], l[1], song) + with open(testFile, "r") as f: + savedSession = loads(f.read()) - sessionFile = MockFile() - saveSession(sessionFile, playlistMock, abControllerMock) + with open(testSessionFile, "r") as f: + testSession = loads(f.read()) - savedSession = loads(sessionFile.read()) assert savedSession == testSession -def test_loadAndSaveEmptySession(playlistMock, abControllerMock): - sessionFile = MockFile() +def test_loadAndSaveEmptySession(songPool, soloTool, tmp_path): + emptyFile = tmp_path / "empty_session.json" - saveSession(sessionFile, playlistMock, abControllerMock) - assert loads(sessionFile.read()) == list() + saveSession(soloTool, emptyFile) + reloadedTool = loadSession(emptyFile, songPool) - loadSession(sessionFile, playlistMock, abControllerMock) - - assert playlistMock == list() - for s in playlistMock: - assert abControllerMock.getStoredLimits(s) == None - -def test_loadSessionNotAdditive(playlistMock, abControllerMock): - sessionFile = MockFile(dumps(testSession)) - loadSession(sessionFile, playlistMock, abControllerMock) - loadSession(sessionFile, playlistMock, abControllerMock) - - songs = playlistMock - 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)) + assert reloadedTool.songs == [] + diff --git a/solo-tool-project/test/solo_tool_controller_integrationtest.py b/solo-tool-project/test/solo_tool_controller_integrationtest.py deleted file mode 100644 index 9311483..0000000 --- a/solo-tool-project/test/solo_tool_controller_integrationtest.py +++ /dev/null @@ -1,86 +0,0 @@ -import pathlib -import shutil -import pytest - -from solo_tool.solo_tool_controller import SoloToolController -from solo_tool.solo_tool import SoloTool - -@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 - -@pytest.fixture -def soloTool(prepared_tmp_path): - st = SoloTool() - st.loadSession(prepared_tmp_path / "test_session.json") - return st - -@pytest.fixture -def uut(soloTool): - return SoloToolController(soloTool) - -def test_previousSong(uut, soloTool): - called = False - receivedValue = None - def callback(value): - nonlocal called, receivedValue - called = True - receivedValue = value - - soloTool.registerCurrentSongCallback(callback) - - soloTool.song == None - assert not called - - uut.previousSong() - soloTool.song == 0 - assert called - assert receivedValue == 0 - called = False - - uut.previousSong() - soloTool.song == 0 - assert not called - - soloTool.song = 1 - uut.previousSong() - soloTool.song == 0 - assert called - assert receivedValue == 0 - called = False - -def test_nextSong(uut, soloTool): - called = False - receivedValue = None - def callback(value): - nonlocal called, receivedValue - called = True - receivedValue = value - - soloTool.registerCurrentSongCallback(callback) - - soloTool.song == None - assert not called - - uut.nextSong() - soloTool.song == 0 - assert called - assert receivedValue == 0 - called = False - - uut.nextSong() - soloTool.song == 1 - assert called - assert receivedValue == 1 - called = False - - uut.nextSong() - soloTool.song == 1 - assert not called diff --git a/solo-tool-project/test/solo_tool_integrationtest.py b/solo-tool-project/test/solo_tool_integrationtest.py index 3a15e36..7b274a3 100644 --- a/solo-tool-project/test/solo_tool_integrationtest.py +++ b/solo-tool-project/test/solo_tool_integrationtest.py @@ -1,358 +1,86 @@ -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 +from fixtures import soloTool as uut, songPool, mockPlayer, testSongs def test_playerControls(uut, mockPlayer): - assert mockPlayer.state == MockPlayer.STOPPED - assert uut.isPlaying() == False + assert not mockPlayer.playing + assert not uut.playing uut.play() - assert mockPlayer.state == MockPlayer.PLAYING - assert uut.isPlaying() == True + assert mockPlayer.playing + assert uut.playing uut.pause() - assert mockPlayer.state == MockPlayer.PAUSED - assert uut.isPlaying() == False - uut.stop() - assert mockPlayer.state == MockPlayer.STOPPED - assert uut.isPlaying() == False + assert not mockPlayer.playing + assert not uut.playing assert mockPlayer.rate == 1.0 - uut.setPlaybackRate(0.5) + uut.rate = 0.5 assert mockPlayer.rate == 0.5 + assert uut.rate == 0.5 assert mockPlayer.position == 0.0 - uut.setPlaybackPosition(0.5) + uut.position = 0.5 assert mockPlayer.position == 0.5 + assert uut.position == 0.5 assert mockPlayer.volume == 1.0 - uut.setPlaybackVolume(0.5) + uut.volume = 0.5 assert mockPlayer.volume == 0.5 + assert uut.volume == 0.5 -def test_addAndSetSongs(uut, mockPlayer): - songs = [ - "test.flac", - "test.mp3" - ] +def test_sanitizePlaybackRate(uut): + # Initial value + assert uut.rate == 1.0 - for s in songs: - uut.addSong(s) - assert mockPlayer.currentSong == None - - for i, s in enumerate(songs): - uut.song = i - assert mockPlayer.currentSong == songs[i] - assert uut.song == i - -def test_addAndSetAbLimits(uut, mockPlayer): - song = "test.flac" - abLimits = [ - [0.2, 0.4], - [0.1, 0.3] - ] - - uut.addSong(song) - uut.song = 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() + # Valid rates are >= 0.0, invalid is ignored + uut.rate = -0.1 + assert uut.rate == 1.0 - 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.song = i - uut.storeAbLimits(l[0], l[1]) - - uut.setAbLimitEnable(True) - - for i, l in enumerate(abLimits): - uut.song = 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.rate = 0.0 + assert uut.rate == 0.0 - 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.song = 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.song = 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.song = 0 - for ab in abLimits: - uut.storeAbLimits(ab[0], ab[1]) - - uut.song = 1 - uut.storeAbLimits(abLimits[0][0], abLimits[0][1]) - - uut.song = 0 - uut.loadAbLimits(len(abLimits) - 1) - checkLimit(uut, mockPlayer, abLimits[-1][0], abLimits[-1][1]) - - uut.song = 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 + uut.rate = 0.0001 + assert uut.rate == 0.0001 -def test_addInexistentFile(uut, mockPlayer): - song = "not/a/real/file" + uut.rate = 150.0 + assert uut.rate == 150.0 - uut.addSong(song) - uut.song = 0 +def test_sanitizePlaybackPosition(uut): + # Initial value + assert uut.position == 0.0 - assert mockPlayer.currentSong == None + # Valid positions are in [0, 1], invalid is limited + uut.position = 0.2 + assert uut.position == 0.2 -def test_getters(uut, mockPlayer): - song = "test.flac" - abLimit = [0.2, 0.4] + uut.position = -0.1 + assert uut.position == 0.0 - uut.addSong(song) - uut.song = 0 - uut.storeAbLimits(abLimit[0], abLimit[1]) + uut.position = 1.0 + assert uut.position == 1.0 - assert uut.songList == [song] + uut.position = 0.4 + assert uut.position == 0.4 - limits = uut.getStoredAbLimits() - assert len(limits) == 1 - assert limits[0][0] == abLimit[0] - assert limits[0][1] == abLimit[1] + uut.position = 1.5 + assert uut.position == 1.0 - mockPlayer.position = 0.8 - assert uut.getPlaybackPosition() == 0.8 +def test_sanitizePlaybackVolume(uut): + # Initial value + assert uut.volume == 1.0 - mockPlayer.volume = 0.8 - assert uut.getPlaybackVolume() == 0.8 + # Valid volumes are >= 0.0, invalid is ignored + uut.volume = -0.1 + assert uut.volume == 1.0 - mockPlayer.rate = 0.5 - assert uut.getPlaybackRate() == 0.5 + uut.volume = 0.0 + assert uut.volume == 0.0 -def test_setTemporaryLimits(uut, mockPlayer): - song = "test.flac" - abLimits = [ - [0.2, 0.4], - [0.1, 0.4] - ] - overflow = 0.5 + uut.volume = 1.0 + assert uut.volume == 1.0 - uut.setAbLimitEnable(True) - mockPlayer.position = overflow - uut.addSong(song) - uut.song = 0 - uut.storeAbLimits(abLimits[0][0], abLimits[0][1]) - uut.loadAbLimits(0) + uut.volume = 150.0 + assert uut.volume == 150.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.song = 0 +def test_playingStateNotification(uut, mockPlayer, testSongs): + uut.addSong(testSongs[0]) called = False receivedValue = None @@ -363,7 +91,7 @@ def test_playingStateNotification(uut, mockPlayer): uut.registerPlayingStateCallback(callback) - assert mockPlayer.state == MockPlayer.STOPPED + assert not mockPlayer.playing assert not called uut.play() @@ -380,22 +108,8 @@ def test_playingStateNotification(uut, mockPlayer): 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.song = 0 +def test_playbackVolumeNotification(uut, mockPlayer, testSongs): + uut.addSong(testSongs[0]) called = False receivedValue = None @@ -404,22 +118,20 @@ def test_playbackVolumeNotification(uut, mockPlayer): called = True receivedValue = value - uut.registerPlaybackVolumeCallback(callback) + uut.registerVolumeCallback(callback) assert not called - uut.setPlaybackVolume(0.3) + uut.volume = 0.3 assert called assert receivedValue == 0.3 called = False - uut.setPlaybackVolume(0.3) + uut.volume = 0.3 assert not called -def test_playbackRateNotification(uut, mockPlayer): - song = "test.flac" - uut.addSong(song) - uut.song = 0 +def test_playbackRateNotification(uut, mockPlayer, testSongs): + uut.addSong(testSongs[0]) called = False receivedValue = None @@ -428,130 +140,15 @@ def test_playbackRateNotification(uut, mockPlayer): called = True receivedValue = value - uut.registerPlaybackRateCallback(callback) + uut.registerRateCallback(callback) assert not called - uut.setPlaybackRate(0.5) + uut.rate = 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.song = 0 - assert called - assert receivedValue == 0 - called = False - - uut.addSong(songs[1]) - assert not called - - uut.song = 0 - assert not called - - uut.song = 1 - assert called - assert receivedValue == 1 - called = False - -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.song = 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() + uut.rate = 0.5 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/solo_tool_keypoints_integrationtest.py b/solo-tool-project/test/solo_tool_keypoints_integrationtest.py new file mode 100644 index 0000000..3db86f5 --- /dev/null +++ b/solo-tool-project/test/solo_tool_keypoints_integrationtest.py @@ -0,0 +1,194 @@ +import pytest + +from fixtures import soloTool as uut, songPool, mockPlayer, testSongs + +def test_keyPointAndSongSelection(uut, mockPlayer, testSongs): + def checkJump(before, expectedAfter): + mockPlayer.position = before + uut.jump() + assert mockPlayer.position == expectedAfter + + # Key point is initially unset + assert uut.keyPoint is None + + # If no song is selected, setting the key point has no effect + assert uut.song is None + uut.keyPoint = 0.5 + assert uut.keyPoint is None + + # With a song selected, key point can be set and jumping works + uut.addSong(testSongs[0]) + uut.keyPoints = [0.3, 0.5] + + uut.keyPoint = 0.6 + assert uut.keyPoint == 0.6 + checkJump(0.8, 0.6) + + # When another song is selected, the key point is set to 0.0 + uut.addSong(testSongs[1]) + uut.song = 1 + assert uut.keyPoint == 0.0 + checkJump(0.5, 0.0) + + # If the selected song has stored key points, the key point is set to the first one instead + uut.song = 0 + assert uut.keyPoint == 0.3 + checkJump(0.5, 0.3) + +def test_keyPointListAndSongSelection(uut, testSongs): + # Key point list is initially unset, since no song is selected + assert uut.keyPoint is None + + # If no song is selected, setting the key point list has no effect + assert uut.song is None + uut.keyPoints = [0.5] + assert uut.keyPoints is None + + # When a song is added, key point list is initialized to empty + uut.addSong(testSongs[0]) + assert uut.keyPoints == [] + + # A new list can be assigned to the song, but it does not affect the current key point + uut.keyPoints = [0.1, 0.3] + assert uut.keyPoints == [0.1, 0.3] + assert uut.keyPoint == 0.0 + + # Each song has its own list of key points + uut.addSong(testSongs[1]) + uut.song = 1 + uut.keyPoints = [0.4] + + uut.song = 0 + assert uut.keyPoints == [0.1, 0.3] + uut.song = 1 + assert uut.keyPoints == [0.4] + +def test_keyPointEdgeCases(uut, testSongs): + uut.addSong(testSongs[0]) + + # Key point cannot be unset + uut.keyPoint = None + assert uut.keyPoint == 0.0 + + # Valid key points are in [0, 1) + uut.keyPoint = -0.1 + assert uut.keyPoint == 0.0 + + uut.keyPoint = 1.0 + assert uut.keyPoint == 0.0 + + uut.keyPoint = 0.999 + assert uut.keyPoint == 0.999 + +def test_keyPointListEdgeCases(uut, testSongs): + uut.addSong(testSongs[0]) + + # Key point list cannot be unset + uut.keyPoints = None + assert uut.keyPoints == [] + + # Appending to the list has no effect + uut.keyPoints.append(0.5) + assert uut.keyPoints == [] + + # Added key points are automatically de-duplicated, sanitized and sorted to ascending order + uut.keyPoints = [0.2, 0.4, 0.1, 0.2, None, -0.5, 1.0, 1.5] + assert uut.keyPoints == [0.1, 0.2, 0.4] + +def test_keyPointSelectionNotification(uut, testSongs): + called = False + receivedValue = None + def callback(value): + nonlocal called, receivedValue + called = True + receivedValue = value + + uut.registerKeyPointSelectionCallback(callback) + assert not called + + # Selecting a song for the first time sets the key point to 0.0 + uut.addSong(testSongs[0]) + assert called + assert receivedValue == 0.0 + called = False + + # Changing the key point triggers a notification + uut.keyPoint = 0.5 + assert called + assert receivedValue == 0.5 + called = False + + # Adding list of key points does not trigger a notification + uut.keyPoints = [0.2, 0.4] + assert not called + + # Assigning the same key point again does not trigger a notification + uut.keyPoint = 0.5 + assert not called + + # Changing song triggers the notification + uut.addSong(testSongs[1]) + uut.song = 1 + assert called + assert receivedValue == 0.0 + called = False + + # But only if the key point really changes + uut.keyPoint = 0.2 + assert called + assert receivedValue == 0.2 + called = False + + uut.song = 0 + assert not called + +def test_keyPointListNotification(uut, testSongs): + called = False + receivedValue = None + def callback(value): + nonlocal called, receivedValue + called = True + receivedValue = value + + uut.registerKeyPointListCallback(callback) + assert not called + + # Adding the first song triggers since the list is now not None + uut.addSong(testSongs[0]) + assert called + assert receivedValue == [] + called = False + + # Adding list of key points triggers + uut.keyPoints = [0.2, 0.4] + assert called + assert receivedValue == [0.2, 0.4] + called = False + + # Same list does not trigger + uut.keyPoints = [0.2, 0.4] + assert called + assert receivedValue == [0.2, 0.4] + called = False + + # Incrementing list of key points triggers after sanitization + uut.keyPoints += [0.2, None, 0.1] + assert called + assert receivedValue == [0.1, 0.2, 0.4] + called = False + + # Changing song triggers + uut.addSong(testSongs[1]) + uut.song = 1 + assert called + assert receivedValue == [] + called = False + + # But only if the list really changed + uut.keyPoints = [0.1, 0.2, 0.4] + assert called + assert receivedValue == [0.1, 0.2, 0.4] + called = False + + uut.song = 0 + assert not called diff --git a/solo-tool-project/test/solo_tool_songs_integrationtest.py b/solo-tool-project/test/solo_tool_songs_integrationtest.py new file mode 100644 index 0000000..6030d19 --- /dev/null +++ b/solo-tool-project/test/solo_tool_songs_integrationtest.py @@ -0,0 +1,141 @@ +import pytest + +from fixtures import soloTool as uut, songPool, mockPlayer, testSongs + +def test_songSelectionFlow(uut, mockPlayer, testSongs): + # Initially, song list is empty and no song is selected + assert uut.song is None + assert mockPlayer.currentSong == None + assert uut.songs == [] + + # When the first song is added, it is selected automatically + uut.addSong(testSongs[0]) + assert uut.song == 0 + assert mockPlayer.currentSong == testSongs[0] + assert uut.songs == testSongs[0:1] + + # Subsequently added songs are not selected automatically + # Song list order is addition order + for i, song in enumerate(testSongs[1:]): + uut.addSong(song) + assert uut.song == 0 + assert mockPlayer.currentSong == testSongs[0] + assert uut.songs == testSongs[0:i + 2] + + # Songs are selected by index + for i, s in enumerate(uut.songs): + uut.song = i + assert uut.song == i + assert mockPlayer.currentSong == uut.songs[i] + +def test_songSelectionEdgeCases(uut, mockPlayer, testSongs): + # When no songs are available, selecting has no effect + uut.song = 0 + assert uut.song == None + assert mockPlayer.currentSong == None + + for song in testSongs: + uut.addSong(song) + + # The current song cannot be de-selected + uut.song = None + assert uut.song == 0 + assert mockPlayer.currentSong == testSongs[0] + + # Non-existent songs cannot be selected + uut.song = -1 + assert uut.song == 0 + assert mockPlayer.currentSong == testSongs[0] + + uut.song = len(testSongs) + assert uut.song == 0 + assert mockPlayer.currentSong == testSongs[0] + +def test_songAdditionEdgeCases(uut, mockPlayer, testSongs): + for song in testSongs: + uut.addSong(song) + + # Modifying the song list directly has no effect + uut.songs.append("something") + assert uut.songs == testSongs + assert mockPlayer.currentSong == testSongs[0] + + # Same song cannot be added twice + uut.addSong(testSongs[0]) + assert uut.song == 0 + assert mockPlayer.currentSong == testSongs[0] + assert uut.songs == testSongs + + # Songs must exist in the filesystem + with pytest.raises(FileNotFoundError): + uut.addSong("/not/a/real/file") + assert uut.song == 0 + assert mockPlayer.currentSong == testSongs[0] + assert uut.songs == testSongs + +def test_songSelectionNotification(uut, testSongs): + selectionCalled = False + selectionValue = None + def selectionCallback(value): + nonlocal selectionCalled, selectionValue + selectionCalled = True + selectionValue = value + + uut.registerSongSelectionCallback(selectionCallback) + assert not selectionCalled + + # Adding the first song triggers because the song is automatically selected + uut.addSong(testSongs[0]) + + assert selectionCalled + assert selectionValue == 0 + selectionCalled = False + + # Adding more songs does not trigger + for i, song in enumerate(testSongs[1:]): + uut.addSong(song) + assert not selectionCalled + + # Selecting another song triggers + uut.song = 1 + assert selectionCalled + assert selectionValue == 1 + selectionCalled = False + + # Selecting the currently selected song does not trigger + uut.song = 1 + assert not selectionCalled + +def test_songListNotification(uut, testSongs): + listCalled = False + listValue = None + def listCallback(value): + nonlocal listCalled, listValue + listCalled = True + listValue = value + + uut.registerSongListCallback(listCallback) + assert not listCalled + + # Adding the first song triggers + uut.addSong(testSongs[0]) + + assert listCalled + assert listValue == testSongs[0:1] + listCalled = False + + # Adding more songs triggers + for i, song in enumerate(testSongs[1:]): + uut.addSong(song) + + assert listCalled + assert listValue == testSongs[0:i + 2] + listCalled = False + + # Modifying the list in place does not trigger + uut.songs.append("something") + assert not listCalled + + # Adding an existing song does not trigger + uut.addSong(testSongs[0]) + assert not listCalled diff --git a/solo-tool-project/test/test.flac b/solo-tool-project/test/test.flac Binary files differdeleted file mode 100644 index 9164735..0000000 --- a/solo-tool-project/test/test.flac +++ /dev/null diff --git a/solo-tool-project/test/test.mp3 b/solo-tool-project/test/test.mp3 Binary files differdeleted file mode 100644 index 3c353b7..0000000 --- a/solo-tool-project/test/test.mp3 +++ /dev/null diff --git a/solo-tool-project/test/test_session.json b/solo-tool-project/test/test_session.json deleted file mode 100644 index f48b792..0000000 --- a/solo-tool-project/test/test_session.json +++ /dev/null @@ -1,13 +0,0 @@ -[ - { - "path" : "test.flac", - "ab_limits" : null - }, - { - "path" : "test.mp3", - "ab_limits" : [ - [0.1, 0.2], - [0.3, 0.4] - ] - } -] |