aboutsummaryrefslogtreecommitdiffstats
path: root/solo-tool-project/src/solo_tool
diff options
context:
space:
mode:
Diffstat (limited to 'solo-tool-project/src/solo_tool')
-rw-r--r--solo-tool-project/src/solo_tool/__init__.py1
-rw-r--r--solo-tool-project/src/solo_tool/abcontroller.py82
-rw-r--r--solo-tool-project/src/solo_tool/midi_controller_launchpad_mini.py145
-rw-r--r--solo-tool-project/src/solo_tool/midi_wrapper_mido.py21
-rw-r--r--solo-tool-project/src/solo_tool/notifier.py29
-rw-r--r--solo-tool-project/src/solo_tool/player_vlc.py55
-rw-r--r--solo-tool-project/src/solo_tool/playlist.py40
-rw-r--r--solo-tool-project/src/solo_tool/session_manager.py41
-rw-r--r--solo-tool-project/src/solo_tool/solo_tool.py164
9 files changed, 578 insertions, 0 deletions
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)
+