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(" bytes: return bytes([id_]) + struct.pack(" 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( " 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)