diff options
Diffstat (limited to 'solo-tool-project')
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.flacBinary files differ new file mode 100644 index 0000000..9164735 --- /dev/null +++ b/solo-tool-project/test/test.flac diff --git a/solo-tool-project/test/test.mp3 b/solo-tool-project/test/test.mp3Binary files differ new file mode 100644 index 0000000..3c353b7 --- /dev/null +++ b/solo-tool-project/test/test.mp3 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] +        ] +    } +] | 
