grid lines and some ui fixes :D
This commit is contained in:
BIN
src/120 bpm track.mp3
Normal file
BIN
src/120 bpm track.mp3
Normal file
Binary file not shown.
@@ -1,3 +1,3 @@
|
|||||||
App {
|
App {
|
||||||
layers: bottom top;
|
layers: top bottom;
|
||||||
}
|
}
|
||||||
0
src/audio.py
Normal file
0
src/audio.py
Normal file
14
src/main.py
14
src/main.py
@@ -1,10 +1,20 @@
|
|||||||
from ui.app import AppUI
|
from ui.app import AppUI
|
||||||
from project import Project
|
from project import Project, ProjectChannel, AudioChannelChunk
|
||||||
|
import librosa
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
print("Loading project...")
|
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
|
# start the ui
|
||||||
print("Starting UI...")
|
print("Starting UI...")
|
||||||
|
|||||||
Binary file not shown.
@@ -9,9 +9,12 @@ from ui.widgets.project_settings import ProjectSettings
|
|||||||
class AppUI(App):
|
class AppUI(App):
|
||||||
CSS_PATH = "../assets/style.tcss"
|
CSS_PATH = "../assets/style.tcss"
|
||||||
|
|
||||||
|
theme = "atom-one-dark"
|
||||||
|
|
||||||
def __init__(self, project):
|
def __init__(self, project):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.zoom_level = 0.05
|
self.zoom_level = 0.05
|
||||||
|
self.last_zoom_level = self.zoom_level
|
||||||
self.project = project
|
self.project = project
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from ui.widgets.chunk_types.chunk import Chunk
|
|||||||
class AudioChunk(Chunk):
|
class AudioChunk(Chunk):
|
||||||
DEFAULT_CSS = """
|
DEFAULT_CSS = """
|
||||||
AudioChunk {
|
AudioChunk {
|
||||||
|
border: tab $accent;
|
||||||
PlotWidget {
|
PlotWidget {
|
||||||
height: 1fr;
|
height: 1fr;
|
||||||
|
|
||||||
@@ -25,8 +26,8 @@ class AudioChunk(Chunk):
|
|||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, audio_data: np.ndarray, sample_rate: int, chunk_name: str = "Sample"):
|
def __init__(self, audio_data: np.ndarray, sample_rate: int, chunk_name: str = "Sample", bar_pos: float = 0.0):
|
||||||
super().__init__(chunk_name)
|
super().__init__(chunk_name, bar_pos)
|
||||||
|
|
||||||
self.audio = audio_data
|
self.audio = audio_data
|
||||||
self.sample_rate = sample_rate
|
self.sample_rate = sample_rate
|
||||||
@@ -42,10 +43,15 @@ 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 = 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):
|
def on_mount(self):
|
||||||
for plot in self.query(PlotWidget):
|
for plot in self.query(PlotWidget):
|
||||||
|
plot: PlotWidget = plot # just for type checking
|
||||||
|
|
||||||
plot.margin_top = 0
|
plot.margin_top = 0
|
||||||
plot.margin_left = 0
|
plot.margin_left = 0
|
||||||
plot.margin_bottom = 0
|
plot.margin_bottom = 0
|
||||||
@@ -85,8 +91,8 @@ class AudioChunk(Chunk):
|
|||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
1.0,
|
1.0,
|
||||||
bar_style=self.app.theme_variables["primary"],
|
bar_style=self.app.theme_variables["warning"],
|
||||||
hires_mode=HiResMode.HALFBLOCK
|
hires_mode=HiResMode.BRAILLE
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,12 +8,23 @@ class Chunk(Container):
|
|||||||
|
|
||||||
DEFAULT_CSS = """
|
DEFAULT_CSS = """
|
||||||
Chunk {
|
Chunk {
|
||||||
border: panel $secondary;
|
border: tab $surface;
|
||||||
background: $surface-darken-1;
|
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__()
|
super().__init__()
|
||||||
self.chunk_name = chunk_name
|
self.chunk_name = chunk_name
|
||||||
self.border_title = chunk_name
|
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
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
from textual.containers import Vertical, VerticalScroll, Horizontal, VerticalGroup, HorizontalGroup
|
from textual.containers import Vertical, VerticalScroll, Horizontal, VerticalGroup, HorizontalGroup
|
||||||
from textual.widgets import Rule
|
from textual.widgets import Rule
|
||||||
from textual.app import ComposeResult
|
from textual.app import ComposeResult
|
||||||
|
from textual import on, events
|
||||||
|
|
||||||
from ui.widgets.chunk_types.audio import AudioChunk, Chunk
|
from ui.widgets.chunk_types.audio import AudioChunk, Chunk
|
||||||
from ui.widgets.play_head import PlayHead
|
from ui.widgets.play_head import PlayHead
|
||||||
@@ -19,7 +20,6 @@ class TimelineRow(Horizontal):
|
|||||||
class Timeline(Vertical):
|
class Timeline(Vertical):
|
||||||
DEFAULT_CSS = """
|
DEFAULT_CSS = """
|
||||||
Timeline {
|
Timeline {
|
||||||
|
|
||||||
#rows {
|
#rows {
|
||||||
hatch: "-" $surface-lighten-1;
|
hatch: "-" $surface-lighten-1;
|
||||||
padding: 1 0;
|
padding: 1 0;
|
||||||
@@ -49,36 +49,68 @@ class Timeline(Vertical):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
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:
|
def compose(self) -> ComposeResult:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
with VerticalScroll(id="rows"):
|
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):
|
for i in range(1, 17):
|
||||||
bar = None
|
bar = None
|
||||||
if (i) % 4 == 0:
|
if i % 4 == 0:
|
||||||
bar = Rule.vertical(classes="bar-line", line_style="double")
|
bar = Rule.vertical(classes="bar-line", line_style="double")
|
||||||
else:
|
else:
|
||||||
bar = Rule.vertical(classes="beat-line")
|
bar = Rule.vertical(classes="beat-line")
|
||||||
|
|
||||||
bar.offset = (self.bar_offset * i, 0)
|
bar.offset = (self.bar_offset * i, 0)
|
||||||
|
bar.index = i
|
||||||
|
|
||||||
yield bar
|
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()
|
#yield PlayHead()
|
||||||
Reference in New Issue
Block a user