feat: reggaeton production system with intelligent sample selection and FLP generation
This commit is contained in:
145
src/flp_builder/writer.py
Normal file
145
src/flp_builder/writer.py
Normal file
@@ -0,0 +1,145 @@
|
||||
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
|
||||
Reference in New Issue
Block a user