226 lines
5.2 KiB
Python
226 lines
5.2 KiB
Python
import struct
|
|
from enum import IntEnum
|
|
|
|
|
|
class EventID(IntEnum):
|
|
WORD = 64
|
|
DWORD = 128
|
|
TEXT = 192
|
|
DATA = 208
|
|
|
|
LoopActive = 9
|
|
ShowInfo = 10
|
|
Volume = 12
|
|
PanLaw = 23
|
|
Licensed = 28
|
|
TempoCoarse = 66
|
|
Pitch = 80
|
|
TempoFine = 93
|
|
CurGroupId = 146
|
|
Tempo = 156
|
|
FLBuild = 159
|
|
Title = 194
|
|
Comments = 195
|
|
Url = 197
|
|
RTFComments = 198
|
|
FLVersion = 199
|
|
Licensee = 200
|
|
DataPath = 202
|
|
Genre = 206
|
|
Artists = 207
|
|
Timestamp = 237
|
|
|
|
ChIsEnabled = 0
|
|
ChVolByte = 2
|
|
ChPanByte = 3
|
|
ChZipped = 15
|
|
ChType = 21
|
|
ChRoutedTo = 22
|
|
ChIsLocked = 32
|
|
ChNew = 64
|
|
ChFreqTilt = 69
|
|
ChFXFlags = 70
|
|
ChCutoff = 71
|
|
ChVolWord = 72
|
|
ChPanWord = 73
|
|
ChPreamp = 74
|
|
ChFadeOut = 75
|
|
ChFadeIn = 76
|
|
ChResonance = 83
|
|
ChStereoDelay = 85
|
|
ChPogo = 86
|
|
ChTimeShift = 89
|
|
ChChildren = 94
|
|
ChSwing = 97
|
|
ChRingMod = 131
|
|
ChCutGroup = 132
|
|
ChRootNote = 135
|
|
ChDelayModXY = 138
|
|
ChReverb = 139
|
|
ChStretchTime = 140
|
|
ChFineTune = 142
|
|
ChSamplerFlags = 143
|
|
ChLayerFlags = 144
|
|
ChGroupNum = 145
|
|
ChAUSampleRate = 153
|
|
ChName = 192
|
|
ChSamplePath = 196
|
|
ChDelay = 209
|
|
ChParameters = 215
|
|
ChEnvelopeLFO = 218
|
|
ChLevels = 219
|
|
ChPolyphony = 221
|
|
ChTracking = 228
|
|
ChLevelAdjusts = 229
|
|
ChAutomation = 234
|
|
|
|
PatLooped = 26
|
|
PatNew = 65
|
|
PatColor = 150
|
|
PatName = 193
|
|
PatChannelIID = 160
|
|
PatLength = 164
|
|
PatControllers = 223
|
|
PatNotes = 224
|
|
|
|
PluginColor = 128
|
|
PluginIcon = 155
|
|
PluginInternalName = 201
|
|
PluginName = 203
|
|
PluginWrapper = 212
|
|
PluginData = 213
|
|
|
|
MixerAPDC = 29
|
|
MixerParams = 225
|
|
|
|
|
|
def encode_varint(value: int) -> bytes:
|
|
result = bytearray()
|
|
while True:
|
|
byte = value & 0x7F
|
|
value >>= 7
|
|
if value:
|
|
byte |= 0x80
|
|
result.append(byte)
|
|
if not value:
|
|
break
|
|
return bytes(result)
|
|
|
|
|
|
def encode_text(text: str, utf16: bool = True) -> bytes:
|
|
if utf16:
|
|
return text.encode("utf-16-le") + b"\x00\x00"
|
|
return text.encode("ascii") + b"\x00"
|
|
|
|
|
|
def encode_byte_event(id_: int, value: int) -> bytes:
|
|
return bytes([id_, value & 0xFF])
|
|
|
|
|
|
def encode_word_event(id_: int, value: int) -> bytes:
|
|
return bytes([id_]) + struct.pack("<H", value)
|
|
|
|
|
|
def encode_dword_event(id_: int, value: int) -> bytes:
|
|
return bytes([id_]) + struct.pack("<I", value)
|
|
|
|
|
|
def encode_text_event(id_: int, text: str) -> bytes:
|
|
data = encode_text(text)
|
|
return bytes([id_]) + encode_varint(len(data)) + data
|
|
|
|
|
|
def encode_data_event(id_: int, data: bytes) -> bytes:
|
|
return bytes([id_]) + encode_varint(len(data)) + data
|
|
|
|
|
|
def encode_note_24(
|
|
position: int,
|
|
flags: int,
|
|
rack_channel: int,
|
|
length: int,
|
|
key: int,
|
|
group: int,
|
|
fine_pitch: int,
|
|
release: int,
|
|
midi_channel: int,
|
|
pan: int,
|
|
velocity: int,
|
|
mod_x: int,
|
|
mod_y: int,
|
|
) -> bytes:
|
|
"""Encode a single note in FL Studio's 24-byte format.
|
|
|
|
Format (24 bytes, all absolute values):
|
|
position: uint32 (4) - absolute position in PPQ ticks
|
|
flags: uint16 (2) - note flags (0x4000 = standard note)
|
|
rack_channel: uint16 (2) - channel rack index
|
|
length: uint32 (4) - duration in PPQ ticks
|
|
key: uint16 (2) - MIDI note number (0-127)
|
|
group: uint16 (2) - note group
|
|
fine_pitch: uint8 (1) - fine pitch (0x78 = 120 = no detune)
|
|
_u1: uint8 (1) - unknown (0x40)
|
|
release: uint8 (1) - release value
|
|
midi_channel: uint8 (1) - MIDI channel
|
|
pan: int8 (1) - stereo pan (64 = center)
|
|
velocity: uint8 (1) - note velocity
|
|
mod_x: uint8 (1) - modulation X (128 = center)
|
|
mod_y: uint8 (1) - modulation Y (128 = center)
|
|
"""
|
|
return struct.pack(
|
|
"<IHHIHHBBBBBBBB",
|
|
position,
|
|
flags,
|
|
rack_channel,
|
|
length,
|
|
key,
|
|
group,
|
|
fine_pitch,
|
|
0x40, # unknown byte, always 0x40 in observed data
|
|
release,
|
|
midi_channel,
|
|
pan,
|
|
velocity,
|
|
mod_x,
|
|
mod_y,
|
|
)
|
|
|
|
|
|
def encode_notes_block(
|
|
channel_index: int,
|
|
notes: list[dict],
|
|
ppq: int = 96,
|
|
) -> bytes:
|
|
"""Encode all notes for a pattern as raw note data (no header).
|
|
|
|
FL Studio stores notes as a flat array of 24-byte structs.
|
|
No header or count prefix needed - the event size determines count.
|
|
"""
|
|
note_data = bytearray()
|
|
|
|
for note in notes:
|
|
pos = int(note.get("position", 0) * ppq)
|
|
length = int(note.get("length", 1) * ppq)
|
|
key = note.get("key", 60)
|
|
velocity = note.get("velocity", 100)
|
|
rack_channel = note.get("rack_channel", channel_index)
|
|
|
|
note_bytes = encode_note_24(
|
|
position=pos,
|
|
flags=0x4000,
|
|
rack_channel=rack_channel,
|
|
length=max(length, 1),
|
|
key=key & 0x7F,
|
|
group=0,
|
|
fine_pitch=120,
|
|
release=64,
|
|
midi_channel=0,
|
|
pan=64,
|
|
velocity=velocity & 0x7F,
|
|
mod_x=128,
|
|
mod_y=128,
|
|
)
|
|
note_data.extend(note_bytes)
|
|
|
|
return bytes(note_data)
|