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

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)