From da2580351933ec3b1360657932b3830bb29fd14c Mon Sep 17 00:00:00 2001 From: SpookyDervish Date: Wed, 14 Jan 2026 09:11:09 +1100 Subject: [PATCH] project serialization --- requirements.txt | 3 +- src/main.py | 14 +++++- src/project.py | 124 ++++++++++++++++++++++++++++++++++++++++++++++- src/ui/app.py | 4 +- 4 files changed, 140 insertions(+), 5 deletions(-) diff --git a/requirements.txt b/requirements.txt index 7d1ca4f..d62e70c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,5 @@ pymp3 textual textual-slider textual-plot -numpy \ No newline at end of file +numpy +msgpack-numpy \ No newline at end of file diff --git a/src/main.py b/src/main.py index 2d42e20..54b26f6 100644 --- a/src/main.py +++ b/src/main.py @@ -1,7 +1,19 @@ from ui.app import AppUI +from project import Project, ProjectChannel, ChannelChunk, AudioChannelChunk +from ui.widgets.chunk_types.audio import AudioChunk +import librosa if __name__ == "__main__": + test_project = Project( + [ + ProjectChannel("my channel", chunks=[ + ChannelChunk(name="hi"), + AudioChannelChunk(librosa.load("cool sample.mp3", sr=None, mono=False)) + ]) + ] + ) + # start the ui - app = AppUI() + app = AppUI(test_project) app.run() \ No newline at end of file diff --git a/src/project.py b/src/project.py index c9ce23d..a00dd50 100644 --- a/src/project.py +++ b/src/project.py @@ -1,3 +1,123 @@ +import msgpack +import enum +import numpy as np +import msgpack_numpy +from dataclasses import dataclass, asdict + + +msgpack_numpy.patch() + +@dataclass +class TimeSignature: + beats_per_measure: float + note_value: float + +class ChunkType(enum.Enum): + CHUNK = 1 + AUDIO = enum.auto() + MIDI = enum.auto() + +class ChannelChunk: + def __init__(self, position: float = 0.0, name: str = "Chunk", chunk_type: ChunkType = ChunkType.CHUNK): + self.position = position # position is how many bars into the song the chunk is + self.name = name + self.chunk_type = chunk_type + + def from_json(json: dict) -> ChannelChunk: + return ChannelChunk( + chunk_type = ChunkType(json["type"]), + name = json["name"], + position = json["position"] + ) + + def to_json(self): + return { + "type": self.chunk_type.value, + "name": self.name, + "position": self.position + } + +class AudioChannelChunk(ChannelChunk): + def __init__(self, audio_data: np.ndarray, position: float = 0.0, name: str = "Sample"): + super().__init__(position, name, chunk_type=ChunkType.AUDIO) + self.audio_data = audio_data + + def from_json(json: dict) -> ChannelChunk: + return AudioChannelChunk( + name = json["name"], + position = json["position"], + audio_data = json["audio_data"] + ) + + def to_json(self): + return { + "type": self.chunk_type.value, + "name": self.name, + "position": self.position, + "audio_data": self.audio_data + } + +chunk_type_associations = { + ChunkType.CHUNK: ChannelChunk, + ChunkType.AUDIO: AudioChannelChunk +} + +class ProjectChannel: + def __init__(self, name: str = "", volume: int = 0, pan: int = 0, mute: bool = False, solo: bool = False, chunks: list[ChannelChunk] = []): + self.name = name + self.volume = volume + self.pan = pan + self.mute = mute + self.solo = solo + self.chunks = chunks + + def from_json(json: dict) -> ProjectChannel: + return ProjectChannel( + name = json["name"], + volume = json["volume"], + pan = json["pan"], + mute = json["mute"], + solo = json["solo"], + chunks = [chunk_type_associations[ChunkType(chunk["type"])].from_json(chunk) for chunk in json["chunks"]] + ) + + def to_json(self): + return { + "name": self.name, + "volume": self.volume, + "pan": self.pan, + "mute": self.mute, + "solo": self.solo, + "chunks": [chunk.to_json() for chunk in self.chunks] + } + class Project: - def __init__(self): - pass \ No newline at end of file + def __init__(self, channels: list[ProjectChannel], version: float = 1.0, bpm: float = 120, time_signature: TimeSignature = TimeSignature(4, 4)): + self.version = version + self.bpm = bpm + self.time_signature = time_signature + self.channels = channels + + def from_file(file_path: str) -> Project: + with open(file_path, "rb") as f: + return Project.from_json(msgpack.unpackb(f.read())) + + def from_json(json: dict) -> Project: + return Project( + version = json["version"], + time_signature = TimeSignature(json["time_signature"]["beats_per_measure"], json["time_signature"]["note_value"]), + bpm = json["bpm"], + channels = [ProjectChannel.from_json(channel) for channel in json["channels"]] + ) + + def to_json(self): + return { + "version": self.version, + "time_signature": asdict(self.time_signature), + "bpm": self.bpm, + "channels": [channel.to_json() for channel in self.channels] + } + + def write_to_file(self, file_path: str): + with open(file_path, "wb") as f: + f.write(msgpack.packb(self.to_json())) \ No newline at end of file diff --git a/src/ui/app.py b/src/ui/app.py index 7217aa0..ee6ce7f 100644 --- a/src/ui/app.py +++ b/src/ui/app.py @@ -7,9 +7,11 @@ from ui.widgets.project_settings import ProjectSettings class AppUI(App): - def __init__(self): + def __init__(self, project): super().__init__() self.zoom_level = 0.05 + self.project = project + def compose(self) -> ComposeResult: yield Sidebar()