WE CAN HEAR THE AUDIO!!!! but uhh rendering to a file is cooked lmao

This commit is contained in:
2026-01-15 05:51:52 +11:00
parent 827f39afcc
commit 6350de7899
9 changed files with 151 additions and 48 deletions

View File

@@ -1,18 +1,33 @@
print("=== TerminalDAW - Version 0.0.1 ===\n")
from ui.app import AppUI from ui.app import AppUI
from project import Project, ProjectChannel, AudioChannelChunk from project import Project, ProjectChannel, AudioChannelChunk
import librosa import librosa
import sounddevice
import mp3
if __name__ == "__main__": if __name__ == "__main__":
print("Loading project...") print("Loading project...")
"""test_project = Project(channels=[ """test_project = Project(song_length=2)
ProjectChannel(chunks=[
AudioChannelChunk(*librosa.load("120 bpm amen break.mp3", mono=False), position=0, name="120 bpm amen break.mp3"),
], name="drums"),
])
#test_project.write_to_file("test_project.tdp")"""
test_project = Project.from_file("test_project.tdp")
drum_channel = ProjectChannel(
test_project,
name="Drums",
volume=5,
)
drum_channel.chunks.append(AudioChannelChunk(
drum_channel,
position=0,
*librosa.load("120 bpm amen break.mp3", mono=False, sr=test_project.sample_rate),
name="120 bpm amen break.mp3"
))
test_project.channels.append(drum_channel)
test_project.write_to_file("test_project.tdp")"""
test_project = Project.from_file("test_project.tdp")
# start the ui # start the ui
print("Starting UI...") print("Starting UI...")

0
src/output.mp3 Normal file
View File

View File

@@ -2,6 +2,7 @@ import msgpack
import enum import enum
import numpy as np import numpy as np
import msgpack_numpy import msgpack_numpy
import pedalboard
from dataclasses import dataclass, asdict from dataclasses import dataclass, asdict
@@ -18,13 +19,18 @@ class ChunkType(enum.Enum):
MIDI = enum.auto() MIDI = enum.auto()
class ChannelChunk: class ChannelChunk:
def __init__(self, position: float = 0.0, name: str = "Chunk", chunk_type: ChunkType = ChunkType.CHUNK): def __init__(self, channel, position: float = 0.0, name: str = "Chunk", chunk_type: ChunkType = ChunkType.CHUNK):
self.channel = channel
self.position = position # position is how many bars into the song the chunk is self.position = position # position is how many bars into the song the chunk is
self.name = name self.name = name
self.chunk_type = chunk_type self.chunk_type = chunk_type
def from_json(json: dict) -> ChannelChunk: def render(self):
pass
def from_json(json: dict, channel) -> ChannelChunk:
return ChannelChunk( return ChannelChunk(
channel,
chunk_type = ChunkType(json["type"]), chunk_type = ChunkType(json["type"]),
name = json["name"], name = json["name"],
position = json["position"] position = json["position"]
@@ -38,13 +44,27 @@ class ChannelChunk:
} }
class AudioChannelChunk(ChannelChunk): class AudioChannelChunk(ChannelChunk):
def __init__(self, audio_data: np.ndarray, sample_rate: int, position: float = 0.0, name: str = "Sample"): def __init__(self, channel, audio_data: np.ndarray, sample_rate: int, position: float = 0.0, name: str = "Sample"):
super().__init__(position, name, chunk_type=ChunkType.AUDIO) super().__init__(channel, position, name, chunk_type=ChunkType.AUDIO)
self.audio_data = audio_data self.audio_data = audio_data
self.sample_rate = sample_rate self.sample_rate = sample_rate
def from_json(json: dict) -> ChannelChunk: def render(self):
start_sample = int(self.position * self.channel.project.samples_per_bar)
audio = self.audio_data.T
# ensure stereo
if audio.ndim == 1:
audio = np.stack([audio, audio], axis=1)
end_sample = start_sample + len(audio)
return start_sample, end_sample, audio
def from_json(json: dict, channel) -> ChannelChunk:
return AudioChannelChunk( return AudioChannelChunk(
channel,
name = json["name"], name = json["name"],
position = json["position"], position = json["position"],
audio_data = json["audio_data"], audio_data = json["audio_data"],
@@ -66,7 +86,8 @@ chunk_type_associations = {
} }
class ProjectChannel: class ProjectChannel:
def __init__(self, name: str = "", volume: int = 0, pan: int = 0, mute: bool = False, solo: bool = False, chunks: list[ChannelChunk] = []): def __init__(self, project, name: str = "", volume: int = 0, pan: int = 0, mute: bool = False, solo: bool = False, chunks: list[ChannelChunk] = []):
self.project = project
self.name = name self.name = name
self.volume = volume self.volume = volume
self.pan = pan self.pan = pan
@@ -74,16 +95,57 @@ class ProjectChannel:
self.solo = solo self.solo = solo
self.chunks = chunks self.chunks = chunks
def from_json(json: dict) -> ProjectChannel: self.board = pedalboard.Pedalboard([
return ProjectChannel( pedalboard.Reverb()
])
def pan_stereo(self, stereo, pan):
pan = np.clip(pan, -1.0, 1.0)
left_gain = np.cos((pan + 1) * np.pi / 4)
right_gain = np.sin((pan + 1) * np.pi / 4)
out = stereo.copy()
out[:, 0] *= left_gain
out[:, 1] *= right_gain
return out
def render(self):
buffer = np.zeros((self.project.total_song_samples, 2), dtype=np.float32)
# render each chunk
for chunk in self.chunks:
start, end, audio = chunk.render()
buffer[start:end] += audio
# apply effects
buffer = self.board(buffer, self.project.sample_rate)
# apply volume
gain = 10 ** (self.volume / 20)
buffer *= gain
# pan
self.pan_stereo(buffer, self.pan/100)
return buffer
def from_json(json: dict, project) -> ProjectChannel:
channel = ProjectChannel(
project,
name = json["name"], name = json["name"],
volume = json["volume"], volume = json["volume"],
pan = json["pan"], pan = json["pan"],
mute = json["mute"], mute = json["mute"],
solo = json["solo"], solo = json["solo"],
chunks = [chunk_type_associations[ChunkType(chunk["type"])].from_json(chunk) for chunk in json["chunks"]]
) )
channel.chunks = [chunk_type_associations[ChunkType(chunk["type"])].from_json(chunk, channel) for chunk in json["chunks"]]
return channel
def to_json(self): def to_json(self):
return { return {
"name": self.name, "name": self.name,
@@ -95,30 +157,51 @@ class ProjectChannel:
} }
class Project: class Project:
def __init__(self, channels: list[ProjectChannel], version: float = 1.0, bpm: float = 120, time_signature: TimeSignature = TimeSignature(4, 4)): def __init__(self, channels: list[ProjectChannel] = [], version: float = 1.0, bpm: float = 120, time_signature: TimeSignature = TimeSignature(4, 4), song_length: float = 16, sample_rate: int = 44100):
self.version = version self.version = version
self.bpm = bpm self.bpm = bpm
self.sample_rate = sample_rate
self.time_signature = time_signature self.time_signature = time_signature
self.channels = channels self.channels = channels
self.song_length = song_length # length of the song in bars
self.seconds_per_bar = (60.0 / self.bpm) * self.time_signature.beats_per_measure
self.samples_per_bar = int(self.seconds_per_bar * self.sample_rate)
self.total_song_samples = int(self.samples_per_bar * self.song_length)
def render(self):
buffer = np.zeros((self.total_song_samples, 2), dtype=np.float32)
for channel in self.channels:
buffer += channel.render()
return buffer
def from_file(file_path: str) -> Project: def from_file(file_path: str) -> Project:
with open(file_path, "rb") as f: with open(file_path, "rb") as f:
return Project.from_json(msgpack.unpackb(f.read())) return Project.from_json(msgpack.unpackb(f.read()))
def from_json(json: dict) -> Project: def from_json(json: dict) -> Project:
return Project( project = Project(
version = json["version"], version = json["version"],
time_signature = TimeSignature(json["time_signature"]["beats_per_measure"], json["time_signature"]["note_value"]), time_signature = TimeSignature(json["time_signature"]["beats_per_measure"], json["time_signature"]["note_value"]),
bpm = json["bpm"], bpm = json["bpm"],
channels = [ProjectChannel.from_json(channel) for channel in json["channels"]] song_length = json["song_length"],
sample_rate = json["sample_rate"]
) )
project.channels = [ProjectChannel.from_json(channel, project) for channel in json["channels"]]
return project
def to_json(self): def to_json(self):
return { return {
"version": self.version, "version": self.version,
"time_signature": asdict(self.time_signature), "time_signature": asdict(self.time_signature),
"bpm": self.bpm, "bpm": self.bpm,
"channels": [channel.to_json() for channel in self.channels] "channels": [channel.to_json() for channel in self.channels],
"song_length": self.song_length,
"sample_rate": self.sample_rate
} }
def write_to_file(self, file_path: str): def write_to_file(self, file_path: str):

View File

@@ -1,13 +0,0 @@
from project import Project
import pedalboard
import sounddevice as sd
import numpy as np
from textual.app import App
class SongPlayer:
def __init__(self, app: App):
self.app = app
def play_song(self, project: Project):
pass

Binary file not shown.

View File

@@ -8,14 +8,11 @@ from ui.widgets.channel import Channel
from project import ProjectChannel from project import ProjectChannel
from song_player import SongPlayer
class AppUI(App): class AppUI(App):
CSS_PATH = "../assets/style.tcss" CSS_PATH = "../assets/style.tcss"
theme = "tokyo-night" theme = "tokyo-night"
#ENABLE_COMMAND_PALETTE = False
def __init__(self, project): def __init__(self, project):
super().__init__() super().__init__()
@@ -23,8 +20,6 @@ class AppUI(App):
self.last_zoom_level = self.zoom_level self.last_zoom_level = self.zoom_level
self.project = project self.project = project
self.song_player = SongPlayer(self)
def create_channel(self, name: str): def create_channel(self, name: str):
self.query_one("#channels").mount(Channel( self.query_one("#channels").mount(Channel(
len(self.project.channels), len(self.project.channels),
@@ -36,9 +31,6 @@ class AppUI(App):
name name
)) ))
def on_mount(self):
self.song_player.play_song(self.app.project)
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
with Tabs(id="top-menu"): with Tabs(id="top-menu"):
yield Tab("File") yield Tab("File")

View File

@@ -55,6 +55,14 @@ class Channel(VerticalGroup):
self.pan = pan self.pan = pan
self.volume = volume self.volume = volume
def on_checkbox_changed(self, event: Checkbox.Changed):
if event.checkbox.id == "mute":
self.muted = event.value
self.app.query_one("#timeline").query_one()
elif event.checkbox.id == "solo":
self.solo = event.value
def on_slider_changed(self, event: Slider.Changed): def on_slider_changed(self, event: Slider.Changed):
if event.slider.id == "volume": if event.slider.id == "volume":
self.volume = round(event.value, 2) self.volume = round(event.value, 2)

View File

@@ -2,6 +2,8 @@ from textual.containers import Horizontal
from textual.app import ComposeResult from textual.app import ComposeResult
from textual.widgets import Button, Input, Static from textual.widgets import Button, Input, Static
import sounddevice as sd
class ProjectSettings(Horizontal): class ProjectSettings(Horizontal):
DEFAULT_CSS = """ DEFAULT_CSS = """
@@ -38,6 +40,10 @@ class ProjectSettings(Horizontal):
super().__init__() super().__init__()
self.border_title = "Project" self.border_title = "Project"
def on_button_pressed(self, event: Button.Pressed):
if event.button.id == "play-button":
sd.play(self.app.project.render())
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
yield Button("", tooltip="Play song", flat=True, id="play-button", variant="success") # icon becomes "⏸" when song is playing yield Button("", tooltip="Play song", flat=True, id="play-button", variant="success") # icon becomes "⏸" when song is playing

View File

@@ -14,17 +14,27 @@ class TimelineRow(Horizontal):
background: $surface-lighten-1; background: $surface-lighten-1;
height: 8; height: 8;
margin-bottom: 1; margin-bottom: 1;
width: 100;
&.-muted {
background: $error 25%;
}
&.-solo {
background: $warning 25%;
}
} }
""" """
class Timeline(Vertical): class Timeline(Vertical):
DEFAULT_CSS = """ DEFAULT_CSS = """
Timeline { Timeline {
overflow-x: auto;
#rows { #rows {
hatch: "-" $surface-lighten-1; hatch: "-" $surface-lighten-1;
padding: 0 0; padding: 0 0;
overflow-x: auto;
.beat-line { .beat-line {
color: $surface-lighten-1; color: $surface-lighten-1;
@@ -58,6 +68,9 @@ class Timeline(Vertical):
def calc_bar_offset(self): def calc_bar_offset(self):
self.bar_offset = self.app.project.bpm / 8 * (0.03333333333 / self.app.zoom_level) self.bar_offset = self.app.project.bpm / 8 * (0.03333333333 / self.app.zoom_level)
for row in self.query(TimelineRow):
row.styles.width = self.bar_offset * self.app.project.song_length
@on(events.MouseScrollDown) @on(events.MouseScrollDown)
async def mouse_scroll_down(self, event: events.MouseScrollDown): async def mouse_scroll_down(self, event: events.MouseScrollDown):
self.app.zoom_level += (self.app.scroll_sensitivity_x / 200) self.app.zoom_level += (self.app.scroll_sensitivity_x / 200)
@@ -93,18 +106,17 @@ class Timeline(Vertical):
with VerticalScroll(id="rows"): with VerticalScroll(id="rows"):
for channel in self.app.project.channels: for channel in self.app.project.channels:
with TimelineRow(): with TimelineRow() as row:
row.styles.width = self.bar_offset * self.app.project.song_length
for chunk in channel.chunks: for chunk in channel.chunks:
if chunk.chunk_type == ChunkType.CHUNK: if chunk.chunk_type == ChunkType.CHUNK:
yield Chunk(chunk_name=chunk.name, bar_pos=chunk.position) yield Chunk(chunk_name=chunk.name, bar_pos=chunk.position)
elif chunk.chunk_type == ChunkType.AUDIO: elif chunk.chunk_type == ChunkType.AUDIO:
yield AudioChunk(chunk.audio_data, chunk.sample_rate, chunk.name, chunk.position) yield AudioChunk(chunk.audio_data, chunk.sample_rate, chunk.name, chunk.position)
for i in range(1, 17): for i in range(1, self.app.project.song_length):
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")