diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..7ebe0f2 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +pymp3 +textual +textual-slider +numpy \ No newline at end of file diff --git a/src/cool sample 2.mp3 b/src/cool sample 2.mp3 new file mode 100644 index 0000000..a1bc816 Binary files /dev/null and b/src/cool sample 2.mp3 differ diff --git a/src/cool sample.mp3 b/src/cool sample.mp3 new file mode 100644 index 0000000..1f0dd23 Binary files /dev/null and b/src/cool sample.mp3 differ diff --git a/src/ui/app.py b/src/ui/app.py index 122fcd9..7928f59 100644 --- a/src/ui/app.py +++ b/src/ui/app.py @@ -6,6 +6,10 @@ from ui.widgets.timeline import Timeline class AppUI(App): + def __init__(self): + super().__init__() + self.zoom_level = 0.1 + def compose(self) -> ComposeResult: yield Sidebar() yield Timeline() diff --git a/src/ui/widgets/chunk_types/audio.py b/src/ui/widgets/chunk_types/audio.py new file mode 100644 index 0000000..57fec27 --- /dev/null +++ b/src/ui/widgets/chunk_types/audio.py @@ -0,0 +1,67 @@ +import librosa +import pyloudnorm as pyln +import math + +from textual.containers import Vertical +from textual.widgets import Sparkline + +from ui.widgets.chunk_types.chunk import Chunk + + + +class AudioChunk(Chunk): + DEFAULT_CSS = """ + AudioChunk { + + Sparkline { + margin: 1; + } + + } + """ + + def __init__(self, file_path: str, 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.num_channels = None + if len(self.audio.shape) == 1: + self.num_samples = self.audio.shape[0] + self.num_channels = 1 + else: + self.num_samples = self.audio.shape[1] + self.num_channels = self.audio.shape[0] + self.notify(str(self.num_samples)) + + self.meter = pyln.Meter(self.sample_rate) + self.loudness_values = [] + + self.styles.width = (self.num_samples / self.sample_rate) / self.app.zoom_level + + + def compose(self) -> ComposeResult: + + + # account for multi-track audio and mono audio + if self.num_channels > 1: + # loop over each channel in the audio and display it seperately + for channel in range(self.num_channels): + + samples = [] + + for sample in range(0, self.num_samples, int(self.sample_rate*0.1)): + samples.append(self.audio[channel, sample]) + + yield Sparkline(data=samples) + else: + # just display the one channel + samples = [] + + for sample in range(0, self.num_samples, int(self.sample_rate*0.1)): + samples.append(self.audio[sample]) + + yield Sparkline(data=samples) + \ No newline at end of file diff --git a/src/ui/widgets/chunk_types/chunk.py b/src/ui/widgets/chunk_types/chunk.py new file mode 100644 index 0000000..b5ee648 --- /dev/null +++ b/src/ui/widgets/chunk_types/chunk.py @@ -0,0 +1,19 @@ +from textual.containers import Container + + +class Chunk(Container): + """ + base class for creating draggable views in the timeline for MIDI, Audio, etc... + """ + + DEFAULT_CSS = """ + Chunk { + border: panel $secondary; + background: $surface-darken-1; + } + """ + + def __init__(self, chunk_name: str = "Chunk"): + super().__init__() + self.chunk_name = chunk_name + self.border_title = chunk_name \ No newline at end of file diff --git a/src/ui/widgets/timeline.py b/src/ui/widgets/timeline.py index 7cb47b7..17895dc 100644 --- a/src/ui/widgets/timeline.py +++ b/src/ui/widgets/timeline.py @@ -1,6 +1,9 @@ -from textual.containers import VerticalScroll, Horizontal +from textual.containers import Vertical, VerticalScroll, Horizontal, VerticalGroup +from textual.widgets import Sparkline from textual.app import ComposeResult +from ui.widgets.chunk_types.audio import AudioChunk + class TimelineRow(Horizontal): DEFAULT_CSS = """ @@ -10,9 +13,6 @@ class TimelineRow(Horizontal): margin-bottom: 1; } """ - - def compose(self) -> ComposeResult: - yield from () class Timeline(VerticalScroll): DEFAULT_CSS = """ @@ -23,5 +23,7 @@ class Timeline(VerticalScroll): """ def compose(self) -> ComposeResult: - yield TimelineRow() - yield TimelineRow() \ No newline at end of file + with TimelineRow(): + yield AudioChunk("cool sample.mp3") + with TimelineRow(): + yield AudioChunk("cool sample 2.mp3") \ No newline at end of file