we have audio rendering working! :D
This commit is contained in:
4
requirements.txt
Normal file
4
requirements.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
pymp3
|
||||||
|
textual
|
||||||
|
textual-slider
|
||||||
|
numpy
|
||||||
BIN
src/cool sample 2.mp3
Normal file
BIN
src/cool sample 2.mp3
Normal file
Binary file not shown.
BIN
src/cool sample.mp3
Normal file
BIN
src/cool sample.mp3
Normal file
Binary file not shown.
@@ -6,6 +6,10 @@ from ui.widgets.timeline import Timeline
|
|||||||
|
|
||||||
|
|
||||||
class AppUI(App):
|
class AppUI(App):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.zoom_level = 0.1
|
||||||
|
|
||||||
def compose(self) -> ComposeResult:
|
def compose(self) -> ComposeResult:
|
||||||
yield Sidebar()
|
yield Sidebar()
|
||||||
yield Timeline()
|
yield Timeline()
|
||||||
|
|||||||
67
src/ui/widgets/chunk_types/audio.py
Normal file
67
src/ui/widgets/chunk_types/audio.py
Normal file
@@ -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)
|
||||||
|
|
||||||
19
src/ui/widgets/chunk_types/chunk.py
Normal file
19
src/ui/widgets/chunk_types/chunk.py
Normal file
@@ -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
|
||||||
@@ -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 textual.app import ComposeResult
|
||||||
|
|
||||||
|
from ui.widgets.chunk_types.audio import AudioChunk
|
||||||
|
|
||||||
|
|
||||||
class TimelineRow(Horizontal):
|
class TimelineRow(Horizontal):
|
||||||
DEFAULT_CSS = """
|
DEFAULT_CSS = """
|
||||||
@@ -11,9 +14,6 @@ class TimelineRow(Horizontal):
|
|||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def compose(self) -> ComposeResult:
|
|
||||||
yield from ()
|
|
||||||
|
|
||||||
class Timeline(VerticalScroll):
|
class Timeline(VerticalScroll):
|
||||||
DEFAULT_CSS = """
|
DEFAULT_CSS = """
|
||||||
Timeline {
|
Timeline {
|
||||||
@@ -23,5 +23,7 @@ class Timeline(VerticalScroll):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def compose(self) -> ComposeResult:
|
def compose(self) -> ComposeResult:
|
||||||
yield TimelineRow()
|
with TimelineRow():
|
||||||
yield TimelineRow()
|
yield AudioChunk("cool sample.mp3")
|
||||||
|
with TimelineRow():
|
||||||
|
yield AudioChunk("cool sample 2.mp3")
|
||||||
Reference in New Issue
Block a user