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 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()

View File

@@ -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 = {

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):
CSS_PATH = "../assets/style.tcss"
def __init__(self, project):
super().__init__()
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)
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):

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

View File

@@ -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;
#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 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():
yield AudioChunk("cool sample.mp3")
with TimelineRow():
yield AudioChunk("cool sample 2.mp3")
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()