making channel controls interactable

This commit is contained in:
2026-01-14 14:43:57 +11:00
parent d757b839d7
commit 7ca336c321
11 changed files with 69 additions and 18 deletions

View File

@@ -3,4 +3,6 @@ textual
textual-slider textual-slider
textual-plot textual-plot
numpy numpy
msgpack-numpy msgpack-numpy
pedalboard
sounddevice

View File

@@ -1,3 +1,8 @@
App { App {
layers: top bottom; layers: top bottom;
}
#top-menu {
dock: top;
layout: horizontal;
} }

View File

View File

@@ -5,16 +5,14 @@ import librosa
if __name__ == "__main__": if __name__ == "__main__":
print("Loading project...") print("Loading project...")
test_project = Project(channels=[ """test_project = Project(channels=[
ProjectChannel(chunks=[ ProjectChannel(chunks=[
AudioChannelChunk(*librosa.load("120 bpm amen break.mp3", mono=False), position=0, name="120 bpm amen break.mp3"), AudioChannelChunk(*librosa.load("120 bpm amen break.mp3", mono=False), position=0, name="120 bpm amen break.mp3"),
AudioChannelChunk(*librosa.load("120 bpm amen break.mp3", mono=False), position=1, name="120 bpm amen break.mp3"),
AudioChannelChunk(*librosa.load("120 bpm amen break.mp3", mono=False), position=2, name="120 bpm amen break.mp3")
], name="drums"), ], name="drums"),
ProjectChannel(chunks=[ ])
AudioChannelChunk(*librosa.load("piano chords - Bmin 120BPM.wav", mono=False), name="piano chords - Bmin 120BPM.wav") #test_project.write_to_file("test_project.tdp")"""
], name="piano") test_project = Project.from_file("test_project.tdp")
])#.from_file("test_project.tdp")
# start the ui # start the ui
print("Starting UI...") print("Starting UI...")

13
src/song_player.py Normal file
View File

@@ -0,0 +1,13 @@
from project import Project
import pedalboard
import sounddevice as sd
import numpy as np
from textual.app import App
class SongPlayer:
def __init__(self, app: App):
self.app = app
def play_song(self, project: Project):
pass

Binary file not shown.

View File

@@ -1,22 +1,36 @@
from textual.app import App, ComposeResult from textual.app import App, ComposeResult
from textual.widgets import Footer from textual.widgets import Footer, Tab, Tabs, Header
from ui.widgets.sidebar import Sidebar from ui.widgets.sidebar import Sidebar
from ui.widgets.timeline import Timeline from ui.widgets.timeline import Timeline
from ui.widgets.project_settings import ProjectSettings from ui.widgets.project_settings import ProjectSettings
from song_player import SongPlayer
class AppUI(App): class AppUI(App):
CSS_PATH = "../assets/style.tcss" CSS_PATH = "../assets/style.tcss"
theme = "tokyo-night"
#ENABLE_COMMAND_PALETTE = False
def __init__(self, project): def __init__(self, project):
super().__init__() super().__init__()
self.zoom_level = 0.05 self.zoom_level = 0.05
self.last_zoom_level = self.zoom_level self.last_zoom_level = self.zoom_level
self.project = project self.project = project
self.song_player = SongPlayer(self)
def on_mount(self):
self.song_player.play_song(self.app.project)
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
with Tabs(id="top-menu"):
yield Tab("File")
yield Tab("Edit")
yield Tab("View")
yield Tab("Help")
yield Sidebar() yield Sidebar()
yield Timeline() yield Timeline()
yield ProjectSettings() yield ProjectSettings()

View File

@@ -4,6 +4,8 @@ from textual.widgets import Checkbox, Input, Static
from textual_slider import Slider from textual_slider import Slider
from textual import on, events
class Channel(VerticalGroup): class Channel(VerticalGroup):
DEFAULT_CSS = """ DEFAULT_CSS = """
@@ -44,23 +46,39 @@ class Channel(VerticalGroup):
} }
""" """
def __init__(self, channel_name: str = "", muted: bool = False, solo: bool = False, pan: float = 0, volume: float = 0): def __init__(self, channel_index: int, channel_name: str = "", muted: bool = False, solo: bool = False, pan: float = 0, volume: float = 0):
super().__init__() super().__init__()
self.channel_index = channel_index
self.channel_name = channel_name self.channel_name = channel_name
self.muted = muted self.muted = muted
self.solo = solo self.solo = solo
self.pan = pan self.pan = pan
self.volume = volume self.volume = volume
def on_slider_changed(self, event: Slider.Changed):
if event.slider.id == "volume":
self.volume = round(event.value, 2)
self.query_one("#volume-text").update(f"Volume ({self.volume}Db):")
elif event.slider.id == "pan":
self.pan = round(event.value)
pan_text = self.query_one("#pan-text")
if self.pan == 0:
pan_text.update("Pan (center):")
elif self.pan < 0:
pan_text.update(f"Pan ({-self.pan}% left):")
elif self.pan > 0:
pan_text.update(f"Pan ({self.pan}% right):")
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
yield Input(placeholder="Channel Name", compact=True, id="name", value=self.channel_name) yield Input(placeholder="Channel Name", compact=True, id="name", value=self.channel_name)
with Horizontal(classes="channel-slider"): with Horizontal(classes="channel-slider"):
yield Static("Pan (center):") yield Static("Pan (center):", id="pan-text")
yield Slider(-100, 100, step=1, value=self.pan) yield Slider(-100, 100, step=1, value=self.pan, id="pan")
with Horizontal(classes="channel-slider"): with Horizontal(classes="channel-slider"):
yield Static("Volume (+ 0Db):") yield Static("Volume (0Db):", id="volume-text")
yield Slider(-15, 15, step=0.1, value=self.volume) yield Slider(-15, 15, step=0.1, value=self.volume, id="volume")
with Horizontal(id="channel-buttons"): with Horizontal(id="channel-buttons"):
yield Checkbox("Mute", compact=True, id="mute", tooltip="Mute this track", value=self.muted) yield Checkbox("Mute", compact=True, id="mute", tooltip="Mute this track", value=self.muted)

View File

@@ -10,9 +10,10 @@ class Sidebar(Vertical):
Sidebar { Sidebar {
dock: left; dock: left;
background: $surface; background: $surface;
width: 40; width: 43;
border-right: tall $surface-lighten-1; border-right: tall $surface-lighten-1;
padding: 1; padding: 1;
margin-top: 1;
#add-channel { #add-channel {
min-width: 100%; min-width: 100%;
@@ -23,8 +24,9 @@ class Sidebar(Vertical):
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
with VerticalScroll(id="channels"): with VerticalScroll(id="channels"):
for channel in self.app.project.channels: for i, channel in enumerate(self.app.project.channels):
yield Channel( yield Channel(
i,
channel.name, channel.name,
channel.mute, channel.mute,
channel.solo, channel.solo,

View File

@@ -95,8 +95,6 @@ class Timeline(Vertical):
for channel in self.app.project.channels: for channel in self.app.project.channels:
self.notify(str(channel))
with TimelineRow(): with TimelineRow():
for chunk in channel.chunks: for chunk in channel.chunks:
if chunk.chunk_type == ChunkType.CHUNK: if chunk.chunk_type == ChunkType.CHUNK:

View File

@@ -0,0 +1 @@
from textual.widgets import Tabs, Tab