Files
reaper-control/src/flp_builder/writer.py

146 lines
5.5 KiB
Python

from __future__ import annotations
import struct
from .events import (
EventID,
encode_byte_event,
encode_word_event,
encode_dword_event,
encode_text_event,
encode_data_event,
encode_varint,
encode_notes_block,
)
from .project import FLPProject, Pattern, Note
class FLPWriter:
def __init__(self, project: FLPProject):
self.project = project
self._events: list[bytes] = []
def build(self) -> bytes:
self._events = []
self._write_project_header()
self._write_patterns()
self._write_channels()
return self._serialize()
def _add_event(self, data: bytes):
self._events.append(data)
def _write_project_header(self):
p = self.project
self._add_event(encode_text_event(EventID.FLVersion, p.fl_version))
self._add_event(encode_dword_event(EventID.FLBuild, 1773))
self._add_event(encode_byte_event(EventID.Licensed, 1))
self._add_event(encode_dword_event(EventID.Tempo, int(p.tempo * 1000)))
self._add_event(encode_byte_event(EventID.LoopActive, 1))
self._add_event(encode_word_event(EventID.Pitch, 0))
self._add_event(encode_byte_event(EventID.PanLaw, 0))
if p.title:
self._add_event(encode_text_event(EventID.Title, p.title))
if p.genre:
self._add_event(encode_text_event(EventID.Genre, p.genre))
if p.artists:
self._add_event(encode_text_event(EventID.Artists, p.artists))
if p.comments:
self._add_event(encode_text_event(EventID.Comments, p.comments))
def _write_patterns(self):
p = self.project
for pat in p.patterns:
self._add_event(encode_word_event(EventID.PatNew, pat.index))
if pat.name:
self._add_event(encode_text_event(EventID.PatName, pat.name))
for ch_idx, notes in pat.notes.items():
if notes:
notes_data = encode_notes_block(
ch_idx,
[n.to_dict() if isinstance(n, Note) else n for n in notes],
ppq=p.ppq,
)
self._add_event(encode_data_event(EventID.PatNotes, notes_data))
def _write_channels(self):
p = self.project
for ch in p.channels:
self._add_event(encode_word_event(EventID.ChNew, ch.index))
self._add_event(encode_byte_event(EventID.ChType, ch.channel_type))
if ch.plugin:
self._add_event(
encode_text_event(EventID.PluginInternalName, ch.plugin.internal_name)
)
if ch.plugin.plugin_data:
self._add_event(
encode_data_event(EventID.PluginData, ch.plugin.plugin_data)
)
elif ch.plugin.internal_name == "Fruity Wrapper":
self._add_event(
encode_text_event(EventID.PluginName, ch.plugin.display_name)
)
wrapper_data = self._build_wrapper_stub(ch.plugin.display_name)
self._add_event(encode_data_event(EventID.PluginData, wrapper_data))
else:
self._add_event(
encode_text_event(EventID.PluginName, ch.plugin.display_name)
)
plugin_data = self._build_native_plugin_stub(ch.plugin.internal_name)
self._add_event(encode_data_event(EventID.PluginData, plugin_data))
if ch.plugin.color:
self._add_event(
encode_dword_event(EventID.PluginColor, ch.plugin.color)
)
self._add_event(encode_text_event(EventID.ChName, ch.name))
self._add_event(encode_byte_event(EventID.ChIsEnabled, 1 if ch.enabled else 0))
self._add_event(encode_byte_event(EventID.ChRoutedTo, ch.mixer_track & 0xFF))
self._add_event(encode_word_event(EventID.ChVolWord, ch.volume))
self._add_event(encode_byte_event(EventID.ChRootNote, ch.root_note))
def _build_wrapper_stub(self, plugin_name: str) -> bytes:
# Minimal VST wrapper state - FL Studio will initialize the plugin fresh
# 10 params with default values
stub = struct.pack("<II", 10, 1) # param_count=10, unknown=1
stub += struct.pack("<II", 20, 0) # version=20, flags=0
stub += b"\xff\xff\xff\xff\xff\xff\xff\xff" # GUID placeholder
stub += b"\x0c\x00\x0c\x00\x0c\x00\x0c\x00" # padding
stub += b"\x00" * 16 # zeros
return stub
def _build_native_plugin_stub(self, internal_name: str) -> bytes:
# Minimal native plugin state
stub = struct.pack("<II", 10, 1)
stub += struct.pack("<II", 20, 0)
stub += b"\xff\xff\xff\xff\xff\xff\xff\xff"
stub += b"\x0c\x00\x0c\x00\x0c\x00\x0c\x00"
stub += b"\x00" * 16
return stub
def _serialize(self) -> bytes:
num_channels = len(self.project.channels)
ppq = self.project.ppq
header = struct.pack(
"<4sIhHH",
b"FLhd",
6,
0,
num_channels,
ppq,
)
all_events = b"".join(self._events)
total_size = len(all_events)
data_header = b"FLdt" + struct.pack("<I", total_size)
return header + data_header + all_events
def write(self, filepath: str):
data = self.build()
with open(filepath, "wb") as f:
f.write(data)
return filepath