From cda8197669409689be291660f93cb288ab2d31b3 Mon Sep 17 00:00:00 2001 From: Eddy Pedroni Date: Sat, 9 Nov 2024 20:35:56 +0100 Subject: Migrate to project-based structure --- solo-tool-project/src/solo_tool/__init__.py | 1 + solo-tool-project/src/solo_tool/abcontroller.py | 82 +++++++++++ .../solo_tool/midi_controller_launchpad_mini.py | 145 ++++++++++++++++++ .../src/solo_tool/midi_wrapper_mido.py | 21 +++ solo-tool-project/src/solo_tool/notifier.py | 29 ++++ solo-tool-project/src/solo_tool/player_vlc.py | 55 +++++++ solo-tool-project/src/solo_tool/playlist.py | 40 +++++ solo-tool-project/src/solo_tool/session_manager.py | 41 ++++++ solo-tool-project/src/solo_tool/solo_tool.py | 164 +++++++++++++++++++++ 9 files changed, 578 insertions(+) create mode 100644 solo-tool-project/src/solo_tool/__init__.py create mode 100644 solo-tool-project/src/solo_tool/abcontroller.py create mode 100644 solo-tool-project/src/solo_tool/midi_controller_launchpad_mini.py create mode 100644 solo-tool-project/src/solo_tool/midi_wrapper_mido.py create mode 100644 solo-tool-project/src/solo_tool/notifier.py create mode 100644 solo-tool-project/src/solo_tool/player_vlc.py create mode 100644 solo-tool-project/src/solo_tool/playlist.py create mode 100644 solo-tool-project/src/solo_tool/session_manager.py create mode 100644 solo-tool-project/src/solo_tool/solo_tool.py (limited to 'solo-tool-project/src/solo_tool') 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) + -- cgit v1.2.3