diff options
| author | Eddy Pedroni <epedroni@pm.me> | 2026-01-01 17:57:27 +0100 |
|---|---|---|
| committer | Eddy Pedroni <epedroni@pm.me> | 2026-01-01 17:57:27 +0100 |
| commit | 8ea2b64ff798af913dcba64baace8d2536bf0b18 (patch) | |
| tree | f85ea2f371055e67c629909df4897aec2f4bbad2 /web-project/src | |
| parent | 88ce99d87889cdf953af611ef09d7a12b6d23747 (diff) | |
Add Android app wrapper around web interface
Diffstat (limited to 'web-project/src')
| -rw-r--r-- | web-project/src/recording.py | 111 | ||||
| -rw-r--r-- | web-project/src/solo_tool_web.py | 18 |
2 files changed, 123 insertions, 6 deletions
diff --git a/web-project/src/recording.py b/web-project/src/recording.py new file mode 100644 index 0000000..2afd567 --- /dev/null +++ b/web-project/src/recording.py @@ -0,0 +1,111 @@ +from pathlib import Path +from contextlib import contextmanager +from asyncio import sleep +from tempfile import TemporaryDirectory +from datetime import date + +from nicegui import ui, run + +_recording = None + +@contextmanager +def _disable(button: ui.button): + button.disable() + try: + yield + finally: + button.enable() + +async def _stopRecording(recordButton, uploadButton, recorder, wavFile): + with _disable(recordButton): + global _recording + _recording = recorder.stopRecording() + await run.cpu_bound(_recording.writeWav, wavFile) + uploadButton.enable() + +def _makeRecordCallback(playButton, recordButton, uploadButton, soloTool, recorder, wavFile): + async def f(): + if recorder.recording: + await _stopRecording(recordButton, uploadButton, recorder, wavFile) + else: + if soloTool.playingAdHoc: + soloTool.backToNormal() + uploadButton.disable() + recorder.startRecording() + playButton.enable() + return f + +def _makePlayCallback(playButton, recordButton, uploadButton, soloTool, recorder, wavFile): + async def f(): + with _disable(playButton): + if recorder.recording: + await _stopRecording(recordButton, uploadButton, recorder, wavFile) + + if soloTool.playingAdHoc: + soloTool.backToNormal() + else: + soloTool.playAdHoc(wavFile) + soloTool.play() + return f + +def _makeUploadCallback(playButton, recordButton, uploadButton, tempDir, sessionManager): + async def f(): + with ui.dialog() as dialog, ui.card(): + fileName = ui.input(label='File name', value="jam.mp3") + with ui.row(): + ui.button('Upload', color='positive', on_click=lambda: dialog.submit(fileName.value)) + ui.button('Cancel', color='negative' ,on_click=lambda: dialog.submit(None)) + + fileName = await dialog + if fileName is None: + return + + playButton.disable() + recordButton.disable() + uploadButton.disable() + + def on_dismiss(): + playButton.enable() + recordButton.enable() + uploadButton.enable() + n = ui.notification(timeout=None, position='bottom-right', type='ongoing', spinner=True, on_dismiss=on_dismiss, icon='check') + + n.message = f'Converting to .mp3...' + mp3File = Path(tempDir.name) / fileName + await run.cpu_bound(_recording.writeMp3, mp3File) + + n.message = 'Uploading...' + folderName = date.today().isoformat() + try: + await run.io_bound(sessionManager.saveRecording, mp3File, f"{folderName}/{fileName}") + except: + n.spinner = False + n.icon = 'error' + n.message = 'Upload failed!' + n.close_button = 'Close' + return + + n.spinner = False + n.message = 'Done!' + await sleep(2) + n.dismiss() + return f + +def recordingControls(soloTool, recorder, sessionManager): + tempDir = TemporaryDirectory(prefix="solotool-") + wavFile = Path(tempDir.name) / "st_recording.wav" + + with ui.button_group().classes('').style('height: 40px'): + recordButton = ui.button(icon='fiber_manual_record', color='negative') \ + .bind_icon_from(recorder, 'recording', lambda recording: 'radio_button_unchecked' if recording else 'fiber_manual_record') + + playButton = ui.button(icon='hearing') \ + .bind_icon_from(soloTool, 'playingAdHoc', lambda adHoc: 'close' if adHoc else 'hearing') + playButton.disable() + + uploadButton = ui.button(icon='cloud_upload') + uploadButton.disable() + + recordButton.on_click(_makeRecordCallback(playButton, recordButton, uploadButton, soloTool, recorder, wavFile)) + playButton.on_click(_makePlayCallback(playButton, recordButton, uploadButton, soloTool, recorder, wavFile)) + uploadButton.on_click(_makeUploadCallback(playButton, recordButton, uploadButton, tempDir, sessionManager)) diff --git a/web-project/src/solo_tool_web.py b/web-project/src/solo_tool_web.py index 3c68585..02ffdac 100644 --- a/web-project/src/solo_tool_web.py +++ b/web-project/src/solo_tool_web.py @@ -10,6 +10,9 @@ from urllib.parse import unquote from solo_tool import SoloTool, handlers from solo_tool.session_manager import SessionManager from solo_tool.midi_controller_actition import ActitionController +from solo_tool.recorder import Recorder + +from recording import recordingControls def fileName(path: str) -> str: return unquote(basename(splitext(path)[0])) @@ -30,6 +33,7 @@ def songList(st: SoloTool, songDrawer) -> None: sessions = {} sessionManager = None midiPedal = ActitionController() +recorder = Recorder(1024, 48000) def makeKeyboardHandler(st: SoloTool): def handleKey(e: events.KeyEventArguments): @@ -103,15 +107,17 @@ def sessionPage(sessionId: str): ui.button(icon='undo', on_click=st.jump, color='secondary').props(f"size={buttonSize}").style('flex: 2') ui.button(icon='skip_next', on_click=handlers.songRelative(st, 1)).props(f"size={buttonSize}").style('flex: 1') - # Playback rate + # Volume slider + with ui.row().classes('w-full justify-between no-wrap items-center'): + volumeLabels = ",".join([f"{v}:'{int(v*100)}%'" for v in [0.0, 0.25, 0.5, 0.75, 1.0, 1.25]]) + ui.slider(min=0, max=1.25, step=0.01).bind_value(st, 'volume').props(f':marker-labels="{{{volumeLabels}}}"').classes('q-px-md') + + # Playback rate and recording controls with ui.row().classes('w-full justify-between no-wrap items-center'): markerLabels = ",".join([f"{v}:'{v}x'" for v in [0.4, 0.6, 0.8, 1.0, 1.2]]) ui.slider(min=0.4, max=1.2, step=0.05).bind_value(st, 'rate').props(f'snap markers :marker-labels="{{{markerLabels}}}"').classes('q-px-md') - # Volume - with ui.row().classes('w-full justify-between no-wrap items-center'): - volumeLabels = ",".join([f"{v}:'{int(v*100)}%'" for v in [0.0, 0.25, 0.5, 0.75, 1.0, 1.25]]) - ui.slider(min=0, max=1.25, step=0.01).bind_value(st, 'volume').props(f':marker-labels="{{{volumeLabels}}}"').classes('q-px-md') + recordingControls(st, recorder, sessionManager) @ui.page('/') def landingPage(): @@ -151,4 +157,4 @@ def main(port, refresh, reload, session_path): # Hardcoded dev settings if __name__ in {"__main__", "__mp_main__"}: start(8080, 0.5, False, "https://files.0xf7.com") - #start(8080, 0.5, True, "/home/eddy/music/sessions") + #start(8080, 0.5, True, "/home/eddy/music") |
