diff --git a/src/120 bpm track.mp3 b/src/120 bpm track.mp3 new file mode 100644 index 0000000..315d89f Binary files /dev/null and b/src/120 bpm track.mp3 differ diff --git a/src/assets/style.tcss b/src/assets/style.tcss index e0cdb13..cc9d1de 100644 --- a/src/assets/style.tcss +++ b/src/assets/style.tcss @@ -1,3 +1,3 @@ App { - layers: bottom top; + layers: top bottom; } \ No newline at end of file diff --git a/src/audio.py b/src/audio.py new file mode 100644 index 0000000..e69de29 diff --git a/src/main.py b/src/main.py index 6c32ff6..ddaef68 100644 --- a/src/main.py +++ b/src/main.py @@ -1,10 +1,20 @@ from ui.app import AppUI -from project import Project +from project import Project, ProjectChannel, AudioChannelChunk +import librosa if __name__ == "__main__": 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 print("Starting UI...") diff --git a/src/test_project.tdp b/src/test_project.tdp index 2cfefce..1b5fe57 100644 Binary files a/src/test_project.tdp and b/src/test_project.tdp differ diff --git a/src/ui/app.py b/src/ui/app.py index 4020b68..cbfd6df 100644 --- a/src/ui/app.py +++ b/src/ui/app.py @@ -9,9 +9,12 @@ from ui.widgets.project_settings import ProjectSettings class AppUI(App): CSS_PATH = "../assets/style.tcss" + theme = "atom-one-dark" + def __init__(self, project): super().__init__() self.zoom_level = 0.05 + self.last_zoom_level = self.zoom_level self.project = project diff --git a/src/ui/widgets/chunk_types/audio.py b/src/ui/widgets/chunk_types/audio.py index 732eba3..c20549a 100644 --- a/src/ui/widgets/chunk_types/audio.py +++ b/src/ui/widgets/chunk_types/audio.py @@ -14,6 +14,7 @@ from ui.widgets.chunk_types.chunk import Chunk class AudioChunk(Chunk): DEFAULT_CSS = """ AudioChunk { + border: tab $accent; PlotWidget { height: 1fr; @@ -25,8 +26,8 @@ class AudioChunk(Chunk): } """ - def __init__(self, audio_data: np.ndarray, sample_rate: int, chunk_name: str = "Sample"): - super().__init__(chunk_name) + def __init__(self, audio_data: np.ndarray, sample_rate: int, chunk_name: str = "Sample", bar_pos: float = 0.0): + super().__init__(chunk_name, bar_pos) self.audio = audio_data self.sample_rate = sample_rate @@ -42,10 +43,15 @@ class AudioChunk(Chunk): self.meter = pyln.Meter(self.sample_rate) 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): for plot in self.query(PlotWidget): + plot: PlotWidget = plot # just for type checking + plot.margin_top = 0 plot.margin_left = 0 plot.margin_bottom = 0 @@ -85,8 +91,8 @@ class AudioChunk(Chunk): x, y, 1.0, - bar_style=self.app.theme_variables["primary"], - hires_mode=HiResMode.HALFBLOCK + bar_style=self.app.theme_variables["warning"], + hires_mode=HiResMode.BRAILLE ) diff --git a/src/ui/widgets/chunk_types/chunk.py b/src/ui/widgets/chunk_types/chunk.py index b5ee648..fc9da32 100644 --- a/src/ui/widgets/chunk_types/chunk.py +++ b/src/ui/widgets/chunk_types/chunk.py @@ -8,12 +8,23 @@ class Chunk(Container): DEFAULT_CSS = """ Chunk { - border: panel $secondary; + border: tab $surface; 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__() self.chunk_name = chunk_name - self.border_title = chunk_name \ No newline at end of file + 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 \ No newline at end of file diff --git a/src/ui/widgets/timeline.py b/src/ui/widgets/timeline.py index fca9f6e..7c99909 100644 --- a/src/ui/widgets/timeline.py +++ b/src/ui/widgets/timeline.py @@ -1,6 +1,7 @@ from textual.containers import Vertical, VerticalScroll, Horizontal, VerticalGroup, HorizontalGroup from textual.widgets import Rule from textual.app import ComposeResult +from textual import on, events from ui.widgets.chunk_types.audio import AudioChunk, Chunk from ui.widgets.play_head import PlayHead @@ -19,7 +20,6 @@ class TimelineRow(Horizontal): class Timeline(Vertical): DEFAULT_CSS = """ Timeline { - #rows { hatch: "-" $surface-lighten-1; padding: 1 0; @@ -49,36 +49,68 @@ class Timeline(Vertical): """ 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: + 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): bar = None - if (i) % 4 == 0: + if i % 4 == 0: bar = Rule.vertical(classes="bar-line", line_style="double") else: bar = Rule.vertical(classes="beat-line") bar.offset = (self.bar_offset * i, 0) + bar.index = i 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() \ No newline at end of file