grid lines and some ui fixes :D

This commit is contained in:
2026-01-14 12:20:53 +11:00
parent a48f758bd0
commit 506d3c8749
9 changed files with 88 additions and 26 deletions

BIN
src/120 bpm track.mp3 Normal file

Binary file not shown.

View File

@@ -1,3 +1,3 @@
App { App {
layers: bottom top; layers: top bottom;
} }

0
src/audio.py Normal file
View File

View File

@@ -1,10 +1,20 @@
from ui.app import AppUI from ui.app import AppUI
from project import Project from project import Project, ProjectChannel, AudioChannelChunk
import librosa
if __name__ == "__main__": if __name__ == "__main__":
print("Loading project...") print("Loading project...")
test_project = Project.from_file("test_project.tdp") test_project = Project(channels=[
ProjectChannel(chunks=[
AudioChannelChunk(*librosa.load("120 bpm amen break.mp3"), position=0),
AudioChannelChunk(*librosa.load("120 bpm amen break.mp3"), position=1),
AudioChannelChunk(*librosa.load("120 bpm amen break.mp3"), position=2)
], name="drums"),
ProjectChannel(chunks=[
], name="piano")
])#.from_file("test_project.tdp")
# start the ui # start the ui
print("Starting UI...") print("Starting UI...")

Binary file not shown.

View File

@@ -9,9 +9,12 @@ from ui.widgets.project_settings import ProjectSettings
class AppUI(App): class AppUI(App):
CSS_PATH = "../assets/style.tcss" CSS_PATH = "../assets/style.tcss"
theme = "atom-one-dark"
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.project = project self.project = project

View File

@@ -14,6 +14,7 @@ from ui.widgets.chunk_types.chunk import Chunk
class AudioChunk(Chunk): class AudioChunk(Chunk):
DEFAULT_CSS = """ DEFAULT_CSS = """
AudioChunk { AudioChunk {
border: tab $accent;
PlotWidget { PlotWidget {
height: 1fr; height: 1fr;
@@ -25,8 +26,8 @@ class AudioChunk(Chunk):
} }
""" """
def __init__(self, audio_data: np.ndarray, sample_rate: int, chunk_name: str = "Sample"): def __init__(self, audio_data: np.ndarray, sample_rate: int, chunk_name: str = "Sample", bar_pos: float = 0.0):
super().__init__(chunk_name) super().__init__(chunk_name, bar_pos)
self.audio = audio_data self.audio = audio_data
self.sample_rate = sample_rate self.sample_rate = sample_rate
@@ -42,10 +43,15 @@ class AudioChunk(Chunk):
self.meter = pyln.Meter(self.sample_rate) self.meter = pyln.Meter(self.sample_rate)
self.loudness_values = [] self.loudness_values = []
self.styles.width = int((self.num_samples / self.sample_rate) / self.app.zoom_level) self.calculate_size()
def calculate_size(self):
self.styles.width = round((self.num_samples / self.sample_rate) / self.app.zoom_level)
def on_mount(self): def on_mount(self):
for plot in self.query(PlotWidget): for plot in self.query(PlotWidget):
plot: PlotWidget = plot # just for type checking
plot.margin_top = 0 plot.margin_top = 0
plot.margin_left = 0 plot.margin_left = 0
plot.margin_bottom = 0 plot.margin_bottom = 0
@@ -85,8 +91,8 @@ class AudioChunk(Chunk):
x, x,
y, y,
1.0, 1.0,
bar_style=self.app.theme_variables["primary"], bar_style=self.app.theme_variables["warning"],
hires_mode=HiResMode.HALFBLOCK hires_mode=HiResMode.BRAILLE
) )

View File

@@ -8,12 +8,23 @@ class Chunk(Container):
DEFAULT_CSS = """ DEFAULT_CSS = """
Chunk { Chunk {
border: panel $secondary; border: tab $surface;
background: $surface-darken-1; background: $surface-darken-1;
dock: left;
} }
""" """
def __init__(self, chunk_name: str = "Chunk"): def __init__(self, chunk_name: str = "Chunk", bar_pos: float = 0.0):
super().__init__() super().__init__()
self.chunk_name = chunk_name self.chunk_name = chunk_name
self.border_title = chunk_name self.border_title = chunk_name
self.bar_pos = bar_pos
self.update_offset()
def update_offset(self, timeline=None):
if timeline == None:
timeline = self.app.query_one("#timeline")
self.offset = (timeline.bar_offset * self.bar_pos * 4, 0)
def calculate_size(self):
pass

View File

@@ -1,6 +1,7 @@
from textual.containers import Vertical, VerticalScroll, Horizontal, VerticalGroup, HorizontalGroup from textual.containers import Vertical, VerticalScroll, Horizontal, VerticalGroup, HorizontalGroup
from textual.widgets import Rule from textual.widgets import Rule
from textual.app import ComposeResult from textual.app import ComposeResult
from textual import on, events
from ui.widgets.chunk_types.audio import AudioChunk, Chunk from ui.widgets.chunk_types.audio import AudioChunk, Chunk
from ui.widgets.play_head import PlayHead from ui.widgets.play_head import PlayHead
@@ -19,7 +20,6 @@ class TimelineRow(Horizontal):
class Timeline(Vertical): class Timeline(Vertical):
DEFAULT_CSS = """ DEFAULT_CSS = """
Timeline { Timeline {
#rows { #rows {
hatch: "-" $surface-lighten-1; hatch: "-" $surface-lighten-1;
padding: 1 0; padding: 1 0;
@@ -49,36 +49,68 @@ class Timeline(Vertical):
""" """
def __init__(self): def __init__(self):
super().__init__() super().__init__(id="timeline")
self.bar_offset = self.app.project.bpm / 8 * (0.0333 / self.app.zoom_level) self.calc_bar_offset()
def calc_bar_offset(self):
self.bar_offset = self.app.project.bpm / 8 * (0.03333333333 / self.app.zoom_level)
@on(events.MouseScrollDown)
async def mouse_scroll_down(self, event: events.MouseScrollDown):
self.app.zoom_level += (self.app.scroll_sensitivity_x / 200)
self.calc_bar_offset()
self.app.last_zoom_level = self.app.zoom_level
self.handle_zoom()
@on(events.MouseScrollUp)
async def mouse_scroll_up(self, event: events.MouseScrollUp):
self.app.zoom_level = max(self.app.zoom_level - self.app.scroll_sensitivity_x/200, 0.001)
if self.app.zoom_level != self.app.last_zoom_level:
self.app.last_zoom_level = self.app.zoom_level
self.calc_bar_offset()
self.handle_zoom()
def handle_zoom(self):
for chunk in self.query(Chunk):
chunk.calculate_size()
chunk.update_offset(self)
for bar_line in self.query(Rule):
if not isinstance(bar_line, PlayHead):
bar_line.offset = (self.bar_offset * bar_line.index, 0)
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
with VerticalScroll(id="rows"): with VerticalScroll(id="rows"):
for channel in self.app.project.channels:
self.notify(str(channel))
with TimelineRow():
for chunk in channel.chunks:
if chunk.chunk_type == ChunkType.CHUNK:
yield Chunk(chunk_name=chunk.name, bar_pos=chunk.position)
elif chunk.chunk_type == ChunkType.AUDIO:
yield AudioChunk(chunk.audio_data, chunk.sample_rate, chunk.name, chunk.position)
for i in range(1, 17): for i in range(1, 17):
bar = None bar = None
if (i) % 4 == 0: if i % 4 == 0:
bar = Rule.vertical(classes="bar-line", line_style="double") bar = Rule.vertical(classes="bar-line", line_style="double")
else: else:
bar = Rule.vertical(classes="beat-line") bar = Rule.vertical(classes="beat-line")
bar.offset = (self.bar_offset * i, 0) bar.offset = (self.bar_offset * i, 0)
bar.index = i
yield bar yield bar
for channel in self.app.project.channels:
with TimelineRow():
for chunk in channel.chunks:
if chunk.chunk_type == ChunkType.CHUNK:
yield Chunk(chunk_name=chunk.name)
elif chunk.chunk_type == ChunkType.AUDIO:
yield AudioChunk(chunk.audio_data, chunk.sample_rate, chunk.name)
#yield PlayHead() #yield PlayHead()