auto generating bar lines, still buggy tho :/

This commit is contained in:
2026-01-14 10:31:26 +11:00
parent da25803519
commit a48f758bd0
10 changed files with 107 additions and 31 deletions

BIN
src/120 bpm amen break.mp3 Normal file

Binary file not shown.

3
src/assets/style.tcss Normal file
View File

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

View File

@@ -1,19 +1,12 @@
from ui.app import AppUI from ui.app import AppUI
from project import Project, ProjectChannel, ChannelChunk, AudioChannelChunk from project import Project
from ui.widgets.chunk_types.audio import AudioChunk
import librosa
if __name__ == "__main__": if __name__ == "__main__":
test_project = Project( print("Loading project...")
[ test_project = Project.from_file("test_project.tdp")
ProjectChannel("my channel", chunks=[
ChannelChunk(name="hi"),
AudioChannelChunk(librosa.load("cool sample.mp3", sr=None, mono=False))
])
]
)
# start the ui # start the ui
print("Starting UI...")
app = AppUI(test_project) app = AppUI(test_project)
app.run() app.run()

View File

@@ -38,15 +38,17 @@ class ChannelChunk:
} }
class AudioChannelChunk(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) super().__init__(position, name, chunk_type=ChunkType.AUDIO)
self.audio_data = audio_data self.audio_data = audio_data
self.sample_rate = sample_rate
def from_json(json: dict) -> ChannelChunk: def from_json(json: dict) -> ChannelChunk:
return AudioChannelChunk( return AudioChannelChunk(
name = json["name"], name = json["name"],
position = json["position"], position = json["position"],
audio_data = json["audio_data"] audio_data = json["audio_data"],
sample_rate = json["sample_rate"]
) )
def to_json(self): def to_json(self):
@@ -54,7 +56,8 @@ class AudioChannelChunk(ChannelChunk):
"type": self.chunk_type.value, "type": self.chunk_type.value,
"name": self.name, "name": self.name,
"position": self.position, "position": self.position,
"audio_data": self.audio_data "audio_data": self.audio_data,
"sample_rate": self.sample_rate
} }
chunk_type_associations = { chunk_type_associations = {

BIN
src/test_project.tdp Normal file

Binary file not shown.

View File

@@ -7,6 +7,8 @@ from ui.widgets.project_settings import ProjectSettings
class AppUI(App): class AppUI(App):
CSS_PATH = "../assets/style.tcss"
def __init__(self, project): def __init__(self, project):
super().__init__() super().__init__()
self.zoom_level = 0.05 self.zoom_level = 0.05

View File

@@ -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) super().__init__(chunk_name)
self.file_path = file_path self.audio = audio_data
self.audio, self.sample_rate = librosa.load(self.file_path, sr=None, mono=False) self.sample_rate = sample_rate
self.num_channels = None self.num_channels = None
if len(self.audio.shape) == 1: if len(self.audio.shape) == 1:
@@ -42,7 +42,7 @@ 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 = (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): def on_mount(self):
for plot in self.query(PlotWidget): for plot in self.query(PlotWidget):

View File

@@ -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")

View File

@@ -23,6 +23,12 @@ class Sidebar(Vertical):
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
with VerticalScroll(id="channels"): with VerticalScroll(id="channels"):
yield Channel() for channel in self.app.project.channels:
yield Channel() yield Channel(
channel.name,
channel.mute,
channel.solo,
channel.pan,
channel.volume
)
yield Button("+ New Channel", variant="success", id="add-channel") yield Button("+ New Channel", variant="success", id="add-channel")

View File

@@ -1,29 +1,84 @@
from textual.containers import Vertical, VerticalScroll, Horizontal, VerticalGroup from textual.containers import Vertical, VerticalScroll, Horizontal, VerticalGroup, HorizontalGroup
from textual.widgets import Sparkline from textual.widgets import Rule
from textual.app import ComposeResult 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): class TimelineRow(Horizontal):
DEFAULT_CSS = """ DEFAULT_CSS = """
TimelineRow { TimelineRow {
background: $surface-lighten-1; background: $surface-lighten-1;
height: 8; max-height: 8;
margin-bottom: 1; margin-bottom: 1;
} }
""" """
class Timeline(VerticalScroll): class Timeline(Vertical):
DEFAULT_CSS = """ DEFAULT_CSS = """
Timeline { Timeline {
padding: 1 0;
#rows {
hatch: "-" $surface-lighten-1; 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: def compose(self) -> ComposeResult:
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(): with TimelineRow():
yield AudioChunk("cool sample.mp3") for chunk in channel.chunks:
with TimelineRow(): if chunk.chunk_type == ChunkType.CHUNK:
yield AudioChunk("cool sample 2.mp3") yield Chunk(chunk_name=chunk.name)
elif chunk.chunk_type == ChunkType.AUDIO:
yield AudioChunk(chunk.audio_data, chunk.sample_rate, chunk.name)
#yield PlayHead()