diff --git a/src/120 bpm amen break.mp3 b/src/120 bpm amen break.mp3 new file mode 100644 index 0000000..035de02 Binary files /dev/null and b/src/120 bpm amen break.mp3 differ diff --git a/src/assets/style.tcss b/src/assets/style.tcss new file mode 100644 index 0000000..e0cdb13 --- /dev/null +++ b/src/assets/style.tcss @@ -0,0 +1,3 @@ +App { + layers: bottom top; +} \ No newline at end of file diff --git a/src/main.py b/src/main.py index 54b26f6..6c32ff6 100644 --- a/src/main.py +++ b/src/main.py @@ -1,19 +1,12 @@ from ui.app import AppUI -from project import Project, ProjectChannel, ChannelChunk, AudioChannelChunk -from ui.widgets.chunk_types.audio import AudioChunk -import librosa +from project import Project if __name__ == "__main__": - test_project = Project( - [ - ProjectChannel("my channel", chunks=[ - ChannelChunk(name="hi"), - AudioChannelChunk(librosa.load("cool sample.mp3", sr=None, mono=False)) - ]) - ] - ) + print("Loading project...") + test_project = Project.from_file("test_project.tdp") # start the ui + print("Starting UI...") app = AppUI(test_project) app.run() \ No newline at end of file diff --git a/src/project.py b/src/project.py index a00dd50..c8d7440 100644 --- a/src/project.py +++ b/src/project.py @@ -38,15 +38,17 @@ class ChannelChunk: } class AudioChannelChunk(ChannelChunk): - def __init__(self, audio_data: np.ndarray, position: float = 0.0, name: str = "Sample"): + def __init__(self, audio_data: np.ndarray, sample_rate: int, position: float = 0.0, name: str = "Sample"): super().__init__(position, name, chunk_type=ChunkType.AUDIO) self.audio_data = audio_data + self.sample_rate = sample_rate def from_json(json: dict) -> ChannelChunk: return AudioChannelChunk( name = json["name"], position = json["position"], - audio_data = json["audio_data"] + audio_data = json["audio_data"], + sample_rate = json["sample_rate"] ) def to_json(self): @@ -54,7 +56,8 @@ class AudioChannelChunk(ChannelChunk): "type": self.chunk_type.value, "name": self.name, "position": self.position, - "audio_data": self.audio_data + "audio_data": self.audio_data, + "sample_rate": self.sample_rate } chunk_type_associations = { diff --git a/src/test_project.tdp b/src/test_project.tdp new file mode 100644 index 0000000..2cfefce Binary files /dev/null and b/src/test_project.tdp differ diff --git a/src/ui/app.py b/src/ui/app.py index ee6ce7f..4020b68 100644 --- a/src/ui/app.py +++ b/src/ui/app.py @@ -7,6 +7,8 @@ from ui.widgets.project_settings import ProjectSettings class AppUI(App): + CSS_PATH = "../assets/style.tcss" + def __init__(self, project): super().__init__() self.zoom_level = 0.05 diff --git a/src/ui/widgets/chunk_types/audio.py b/src/ui/widgets/chunk_types/audio.py index ac65715..732eba3 100644 --- a/src/ui/widgets/chunk_types/audio.py +++ b/src/ui/widgets/chunk_types/audio.py @@ -25,11 +25,11 @@ class AudioChunk(Chunk): } """ - def __init__(self, file_path: str, chunk_name: str = "Sample"): + def __init__(self, audio_data: np.ndarray, sample_rate: int, chunk_name: str = "Sample"): super().__init__(chunk_name) - self.file_path = file_path - self.audio, self.sample_rate = librosa.load(self.file_path, sr=None, mono=False) + self.audio = audio_data + self.sample_rate = sample_rate self.num_channels = None if len(self.audio.shape) == 1: @@ -42,7 +42,7 @@ class AudioChunk(Chunk): self.meter = pyln.Meter(self.sample_rate) self.loudness_values = [] - self.styles.width = (self.num_samples / self.sample_rate) / self.app.zoom_level + self.styles.width = int((self.num_samples / self.sample_rate) / self.app.zoom_level) def on_mount(self): for plot in self.query(PlotWidget): diff --git a/src/ui/widgets/play_head.py b/src/ui/widgets/play_head.py new file mode 100644 index 0000000..769bba6 --- /dev/null +++ b/src/ui/widgets/play_head.py @@ -0,0 +1,14 @@ +from textual.widgets import Rule + + +class PlayHead(Rule): + DEFAULT_CSS = """ + PlayHead.-vertical { + layer: top; + margin: 0; + color: $accent; + } + """ + + def __init__(self): + super().__init__(orientation="vertical") \ No newline at end of file diff --git a/src/ui/widgets/sidebar.py b/src/ui/widgets/sidebar.py index a74974a..00c704e 100644 --- a/src/ui/widgets/sidebar.py +++ b/src/ui/widgets/sidebar.py @@ -23,6 +23,12 @@ class Sidebar(Vertical): def compose(self) -> ComposeResult: with VerticalScroll(id="channels"): - yield Channel() - yield Channel() + for channel in self.app.project.channels: + yield Channel( + channel.name, + channel.mute, + channel.solo, + channel.pan, + channel.volume + ) yield Button("+ New Channel", variant="success", id="add-channel") \ No newline at end of file diff --git a/src/ui/widgets/timeline.py b/src/ui/widgets/timeline.py index 17895dc..fca9f6e 100644 --- a/src/ui/widgets/timeline.py +++ b/src/ui/widgets/timeline.py @@ -1,29 +1,84 @@ -from textual.containers import Vertical, VerticalScroll, Horizontal, VerticalGroup -from textual.widgets import Sparkline +from textual.containers import Vertical, VerticalScroll, Horizontal, VerticalGroup, HorizontalGroup +from textual.widgets import Rule from textual.app import ComposeResult -from ui.widgets.chunk_types.audio import AudioChunk +from ui.widgets.chunk_types.audio import AudioChunk, Chunk +from ui.widgets.play_head import PlayHead +from project import ChunkType class TimelineRow(Horizontal): DEFAULT_CSS = """ TimelineRow { background: $surface-lighten-1; - height: 8; + max-height: 8; margin-bottom: 1; } """ -class Timeline(VerticalScroll): +class Timeline(Vertical): DEFAULT_CSS = """ Timeline { - padding: 1 0; - hatch: "-" $surface-lighten-1; + + #rows { + hatch: "-" $surface-lighten-1; + padding: 1 0; + + .beat-line { + color: $surface-lighten-1; + + } + + .bar-line { + color: $surface-lighten-2; + + } + + .beat-line, .bar-line { + dock: left; + margin: 0; + } + + + } + + PlayHead { + layer: top; + } } """ + def __init__(self): + super().__init__() + + self.bar_offset = self.app.project.bpm / 8 * (0.0333 / self.app.zoom_level) + def compose(self) -> ComposeResult: - with TimelineRow(): - yield AudioChunk("cool sample.mp3") - with TimelineRow(): - yield AudioChunk("cool sample 2.mp3") \ No newline at end of file + + + with VerticalScroll(id="rows"): + + for i in range(1, 17): + bar = None + 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) + + 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