feat: reggaeton production system with intelligent sample selection and FLP generation

This commit is contained in:
renato97
2026-05-02 21:40:18 -03:00
commit 4d941f3f90
62 changed files with 8656 additions and 0 deletions

View File

@@ -0,0 +1,222 @@
"""FL Studio arrangement/playlist encoding.
Encodes playlist items (ID233) and track data (ID238) into binary format
matching FL Studio's internal structure. Extracted from the proven v15 builder
(output/build_reggaeton_v15.py, lines 61-90).
Arrangement block sequence:
ArrNew(99) → ArrName(241) → Flag36(36) → Playlist(233)
→ TrackData(238)×N → ArrCurrent(100)
"""
from dataclasses import dataclass
import struct
from .events import encode_byte_event, encode_data_event, encode_word_event
# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------
PPQ_DEFAULT: int = 96
MAX_TRACKS_DEFAULT: int = 500
PATTERN_BASE: int = 20480
# Arrangement event IDs (not yet in EventID enum — raw constants)
EID_ARR_NEW = 99
EID_ARR_CURRENT = 100
EID_ARR_NAME = 241
EID_FLAG_36 = 36
EID_PLAYLIST = 233
EID_TRACK_DATA = 238
# TrackData template size (bytes), extracted from reference FLP
TRACK_DATA_SIZE = 66
# ---------------------------------------------------------------------------
# ArrangementItem dataclass
# ---------------------------------------------------------------------------
@dataclass
class ArrangementItem:
"""A single playlist item placed on the arrangement timeline.
Args:
pattern_id: Pattern number (1-based).
bar: Start bar (0-based, fractional allowed).
num_bars: Length in bars (fractional allowed).
track_index: Track row index (0-based).
muted: Whether the item is muted in the playlist.
"""
pattern_id: int # pattern number (1-based)
bar: float # start bar (0-based)
num_bars: float # length in bars
track_index: int # 0-based track index
muted: bool = False
def to_bytes(
self,
ppq: int = PPQ_DEFAULT,
max_tracks: int = MAX_TRACKS_DEFAULT,
) -> bytes:
"""Encode as a 32-byte playlist item (ID233 format).
Encoding rules (from reverse-engineered FL Studio format):
position = int(bar × ppq × 4) — ticks, truncated
pattern_base = 20480 — constant
item_index = 20480 + pattern_id
length = int(num_bars × ppq × 4) — ticks, truncated
track_rvidx = (max_tracks - 1) - track_index — REVERSED
flags = 0x2040 if muted else 0x0040
"""
position = int(self.bar * ppq * 4)
item_index = PATTERN_BASE + self.pattern_id
length = int(self.num_bars * ppq * 4)
track_rvidx = (max_tracks - 1) - self.track_index
flags = 0x2040 if self.muted else 0x0040
return struct.pack(
"<IHHIHH HH 4B ff",
position,
PATTERN_BASE,
item_index,
length,
track_rvidx,
0, # group
0x0078,
flags,
64, 100, 128, 128,
-1.0, -1.0,
)
# ---------------------------------------------------------------------------
# TrackData helpers
# ---------------------------------------------------------------------------
def build_track_data_template(reference_flp_bytes: bytes) -> bytes:
"""Extract the 66-byte TrackData template from a reference FLP.
Scans the raw FLP bytes for the first ID238 event and returns its
66-byte payload. This template is then cloned and patched for each
of the *max_tracks* track data entries in the arrangement section.
Args:
reference_flp_bytes: Full contents of a valid .flp file.
Returns:
The 66-byte track-data template.
Raises:
ValueError: If no ID238 event of the expected size is found.
"""
pos = 22 # skip FLhd (14 bytes) + FLdt header (8 bytes)
while pos < len(reference_flp_bytes):
ib = reference_flp_bytes[pos]
pos += 1
if ib < 64:
# Byte event: 1-byte value
pos += 1
elif ib < 128:
# Word event: 2-byte value
pos += 2
elif ib < 192:
# Dword event: 4-byte value
pos += 4
else:
# Data / text event: varint length + payload
size = 0
shift = 0
while True:
b = reference_flp_bytes[pos]
pos += 1
size |= (b & 0x7F) << shift
shift += 7
if not (b & 0x80):
break
if ib == EID_TRACK_DATA and size == TRACK_DATA_SIZE:
return bytes(reference_flp_bytes[pos:pos + size])
pos += size
raise ValueError(
f"No ID{EID_TRACK_DATA} TrackData event ({TRACK_DATA_SIZE} bytes) "
"found in reference FLP"
)
def encode_track_data(iid: int, enabled: int, template: bytes) -> bytes:
"""Clone *template*, patch iid at byte 0 (uint32 LE) and enabled at byte 12.
Args:
iid: Internal track ID (sequential from 1).
enabled: 0 = disabled, 1 = enabled.
template: 66-byte template extracted by :func:`build_track_data_template`.
Returns:
66-byte patched track data.
"""
td = bytearray(template)
struct.pack_into("<I", td, 0, iid)
td[12] = enabled & 0xFF
return bytes(td)
# ---------------------------------------------------------------------------
# Full arrangement section builder
# ---------------------------------------------------------------------------
def build_arrangement_section(
items: list[ArrangementItem],
track_data_template: bytes,
ppq: int = PPQ_DEFAULT,
max_tracks: int = MAX_TRACKS_DEFAULT,
) -> bytes:
"""Build the full post-channel arrangement section bytes.
Produces the exact byte sequence FL Studio expects after the channel
events:
ArrNew(99) → ArrName(241) → Flag36(36) → Playlist(233)
→ TrackData(238) × *max_tracks* → ArrCurrent(100)
Args:
items: Playlist items to encode.
track_data_template: 66-byte template from :func:`build_track_data_template`.
ppq: Pulses-per-quarter-note (default 96).
max_tracks: Total track-data entries to write (default 500).
Returns:
Complete arrangement section as raw bytes.
"""
result = bytearray()
# 1. ArrNew — word event, value = 0
result.extend(encode_word_event(EID_ARR_NEW, 0))
# 2. ArrName — "Arrangement" as UTF-16-LE + null terminator
arr_name = "Arrangement".encode("utf-16-le") + b"\x00\x00"
result.extend(encode_data_event(EID_ARR_NAME, arr_name))
# 3. Flag36 — byte event, value = 0
result.extend(encode_byte_event(EID_FLAG_36, 0))
# 4. Playlist — data event, concatenation of all 32-byte items
pl_data = b"".join(item.to_bytes(ppq, max_tracks) for item in items)
result.extend(encode_data_event(EID_PLAYLIST, pl_data))
# 5. TrackData × max_tracks — first track (iid=1) disabled, rest enabled
for i in range(1, max_tracks + 1):
enabled = 0 if i == 1 else 1
td = encode_track_data(i, enabled, track_data_template)
result.extend(encode_data_event(EID_TRACK_DATA, td))
# 6. ArrCurrent — word event, value = 0
result.extend(encode_word_event(EID_ARR_CURRENT, 0))
return bytes(result)