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(" bytes: # Minimal native plugin state stub = struct.pack(" 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("