From bfad56faaa936cb80ab51fed10e9b89b5df11471 Mon Sep 17 00:00:00 2001 From: Eddy Pedroni Date: Sun, 23 Feb 2025 21:52:32 +0100 Subject: Most web UI features implemented, layout still messy --- doc/diagram.drawio | 429 +-------------------- readme.md | 2 +- solo-tool-project/src/solo_tool/handlers.py | 13 +- .../solo_tool/midi_controller_launchpad_mini.py | 2 +- solo-tool-project/src/solo_tool/solo_tool.py | 5 +- .../test/solo_tool_integrationtest.py | 8 +- web-project/src/solo_tool_web.py | 99 ++--- 7 files changed, 91 insertions(+), 467 deletions(-) diff --git a/doc/diagram.drawio b/doc/diagram.drawio index ee5ea5e..bb8dbd0 100644 --- a/doc/diagram.drawio +++ b/doc/diagram.drawio @@ -1,375 +1,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + @@ -569,12 +200,12 @@ - + - + @@ -589,55 +220,31 @@ - + - - - - - - - - - - - - - - - - - - - + - + - - + + - - + + - - - - - - - - + + - + - - + + - - + + diff --git a/readme.md b/readme.md index b5d1094..20f15b6 100644 --- a/readme.md +++ b/readme.md @@ -1,6 +1,6 @@ # Solo Tool -This tool is designed to facilitate learning songs, and solos in particular, by slowing down playback and automatically repeating short sections of the file. +This tool is designed to facilitate learning songs, and solos in particular, by slowing down playback and automatically jumping to predefined points in the song. ## Dependencies diff --git a/solo-tool-project/src/solo_tool/handlers.py b/solo-tool-project/src/solo_tool/handlers.py index 040928c..1e0e22c 100644 --- a/solo-tool-project/src/solo_tool/handlers.py +++ b/solo-tool-project/src/solo_tool/handlers.py @@ -4,7 +4,7 @@ from solo_tool.solo_tool import SoloTool def playPause(st: SoloTool) -> Callable[[], None]: def f(): - if st.isPlaying(): + if st.playing: st.pause() else: st.play() @@ -28,6 +28,11 @@ def positionToKeyPoint(st: SoloTool) -> Callable[[], None]: st.keyPoint = st.position return f +def setKeyPoint(st: SoloTool, kp: float) -> Callable[[], None]: + def f(): + st.keyPoint = kp + return f + def changeKeyPoint(st: SoloTool, delta: int) -> Callable[[], None]: from bisect import bisect_right, bisect_left def f(): @@ -46,7 +51,13 @@ def rateAbsolute(st: SoloTool, value: float) -> Callable[[], None]: st.rate = value return f +def rateRelative(st: SoloTool, delta: float) -> Callable[[], None]: + def f(): + st.rate += delta + return f + def volumeAbsolute(st: SoloTool, value: float) -> Callable[[], None]: def f(): st.volume = value return f + diff --git a/solo-tool-project/src/solo_tool/midi_controller_launchpad_mini.py b/solo-tool-project/src/solo_tool/midi_controller_launchpad_mini.py index 9582a88..38b7cce 100644 --- a/solo-tool-project/src/solo_tool/midi_controller_launchpad_mini.py +++ b/solo-tool-project/src/solo_tool/midi_controller_launchpad_mini.py @@ -113,7 +113,7 @@ class MidiController: # playback control self._setButtonLED(6, 0, MidiController.LED_RED) - self._updatePlayPauseButton(self._soloTool.isPlaying()) + self._updatePlayPauseButton(self._soloTool.playing) # Key point control self._setButtonLED(7, 2, MidiController.LED_YELLOW) diff --git a/solo-tool-project/src/solo_tool/solo_tool.py b/solo-tool-project/src/solo_tool/solo_tool.py index 13e3c03..0f47aef 100644 --- a/solo-tool-project/src/solo_tool/solo_tool.py +++ b/solo-tool-project/src/solo_tool/solo_tool.py @@ -29,7 +29,7 @@ class SoloTool: def addSong(self, path: str) -> None: if not os.path.isfile(path): - raise FileNotFoundError() + raise FileNotFoundError(path) self._songs.append(path) self._keyPoints.append([]) @@ -76,7 +76,8 @@ class SoloTool: def stop(self): self._player.stop() - def isPlaying(self): + @property + def playing(self) -> bool: return self._player.isPlaying() def jump(self): diff --git a/solo-tool-project/test/solo_tool_integrationtest.py b/solo-tool-project/test/solo_tool_integrationtest.py index 2a818ed..2a55df9 100644 --- a/solo-tool-project/test/solo_tool_integrationtest.py +++ b/solo-tool-project/test/solo_tool_integrationtest.py @@ -27,16 +27,16 @@ def prepared_tmp_path(tmp_path): def test_playerControls(uut, mockPlayer): assert mockPlayer.state == MockPlayer.STOPPED - assert uut.isPlaying() == False + assert uut.playing == False uut.play() assert mockPlayer.state == MockPlayer.PLAYING - assert uut.isPlaying() == True + assert uut.playing == True uut.pause() assert mockPlayer.state == MockPlayer.PAUSED - assert uut.isPlaying() == False + assert uut.playing == False uut.stop() assert mockPlayer.state == MockPlayer.STOPPED - assert uut.isPlaying() == False + assert uut.playing == False assert mockPlayer.rate == 1.0 uut.rate = 0.5 diff --git a/web-project/src/solo_tool_web.py b/web-project/src/solo_tool_web.py index f854e1a..edca008 100644 --- a/web-project/src/solo_tool_web.py +++ b/web-project/src/solo_tool_web.py @@ -1,56 +1,61 @@ from nicegui import ui from solo_tool import SoloTool +from solo_tool.session_manager import loadSession +from solo_tool import handlers +from solo_tool.midi_controller_launchpad_mini import MidiController -st = SoloTool() -st.loadSession("/home/eddy/music/solos/practice.json") - -def _createSeekHandler(delta): - def f(): - newPosition = st.getPlaybackPosition() + delta - newPosition = min(1.0, max(0.0, newPosition)) - st.setPlaybackPosition(newPosition) - return f +st = loadSession("/home/eddy/music/funk-band/practice.json") +midiController = MidiController(st) +midiController.connect() def main(): - with ui.splitter(value=30) as splitter: - splitter.style('width: 100%; height: 100%;') - with splitter.before: - with ui.list().props('dense separator'): - for song in st.getSongs(): - ui.item(song) - with splitter.after: - ui.slider(min=0, max=1.2, value=1.0, step=0.01, on_change=lambda e: st.setPlaybackVolume(e.value)) - - with ui.row().classes("w-full justify-between no-wrap"): - ui.button('-5%', on_click=lambda: st.setPlaybackRate(max(0.5, st.getPlaybackRate() - 0.05))) - ui.slider(min=0.5, max=1.2, step=0.05, value=st.getPlaybackRate(), on_change=lambda e: st.setPlaybackRate(e.value)) - ui.button('+5%', on_click=lambda: st.setPlaybackRate(min(1.2, st.getPlaybackRate() + 0.05))) - - ui.slider(min=0, max=100, value=0) - - with ui.row().classes("w-full justify-between no-wrap"): - ui.button('Prev', on_click=st.previousSong) - ui.button('-25%', on_click=_createSeekHandler(-0.25)) - ui.button('-5%', on_click=_createSeekHandler(-0.05)) - ui.button('-1%', on_click=_createSeekHandler(-0.01)) - ui.button('+1%', on_click=_createSeekHandler(0.01)) - ui.button('+5%', on_click=_createSeekHandler(0.05)) - ui.button('+25%', on_click=_createSeekHandler(0.25)) - ui.button('Next', on_click=st.nextSong) - - with ui.row().classes("w-full justify-between no-wrap"): - ui.button('Set A') - ui.button('Set B') - ui.button('Previous AB') - ui.button('Next AB') - - with ui.row().classes("w-full justify-between no-wrap"): - ui.button('Toggle AB', on_click=lambda: st.setAbLimitEnable(not st.isAbLimitEnabled())) - ui.button('Stop', on_click=st.stop) - ui.button('Play', on_click=st.play) - ui.button('Jump to A', on_click=st.jumpToA) - ui.run() + fullscreen = ui.fullscreen() + ui.dark_mode().enable() + ui.colors(secondary='#ffc107') + + with ui.row() as mainContainer: + mainContainer.style('width: 100%; height: 100%;') + + with ui.column() as keyPointColumn: + keyPointColumn.style('width: 12%; height: 100%;') + + ui.label('Key Points') + + with ui.scroll_area(): + for i, song in enumerate(st.songs): + with ui.list().props('dense separator').bind_visibility_from(st, 'song', value=i) as keyPointList: + for kp in st._keyPoints[i]: + ui.item(f"{kp}", on_click=handlers.setKeyPoint(st, kp)).props('clickable') + + with ui.column() as controlColumn: + controlColumn.style('width: 84%; height: 100%;') + + ui.label("Song name").bind_text_from(st, 'song', lambda index: st.songs[index] if index is not None else "") + + # Playback position + ui.slider(min=0, max=1.0, step=0.01).bind_value(st, 'position').props('thumb-size=0px track-size=16px') + + # Key point position + ui.slider(min=0, max=1.0, step=0.01).bind_value(st, 'keyPoint').props('color=secondary') + + # Play control + with ui.button_group().classes('w-full').style('height: 90px'): + ui.button(icon='skip_previous', on_click=handlers.changeSong(st, -1)).style('flex: 1') + ui.button(icon='stop', on_click=st.stop, color='negative').style('flex: 1') + ui.button(color='positive', on_click=handlers.playPause(st)).bind_icon_from(st, "playing", lambda playing: "pause" if playing else "play_arrow").style('flex: 2') + ui.button(icon='undo', on_click=st.jump, color='secondary').style('flex: 2') + ui.button(icon='skip_next', on_click=handlers.changeSong(st, 1)).style('flex: 1') + + # Volume and rate + with ui.row().classes('w-full justify-between no-wrap'): + ui.button(icon='fast_rewind', on_click=handlers.rateRelative(st, -0.05)) + ui.label().bind_text_from(st, 'rate', lambda rate: f'{rate:0.3}').on('click', handlers.rateAbsolute(st, 1.0)) + ui.button(icon='fast_forward', on_click=handlers.rateRelative(st, 0.05)) + ui.slider(min=0, max=1.2, step=0.01).bind_value(st, 'volume') + ui.button(icon='fullscreen', on_click=fullscreen.toggle).props('outline') + + ui.run() if __name__ in {'__main__', '__mp_main__'}: main() -- cgit v1.2.3