Skip to content

Commit cddd8ef

Browse files
committedJun 26, 2023
Steal from MIDIProgramSplitter
0 parents  commit cddd8ef

29 files changed

+3483
-0
lines changed
 

‎.gitattributes

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
###############################################################################
2+
# Set default behavior to automatically normalize line endings.
3+
###############################################################################
4+
* text eol=lf
5+
6+
###############################################################################
7+
# Set default behavior for command prompt diff.
8+
#
9+
# This is need for earlier builds of msysgit that does not have it on by
10+
# default for csharp files.
11+
# Note: This is only used by command line
12+
###############################################################################
13+
#*.cs diff=csharp
14+
15+
###############################################################################
16+
# Set the merge driver for project and solution files
17+
#
18+
# Merging from the command prompt will add diff markers to the files if there
19+
# are conflicts (Merging from VS is not affected by the settings below, in VS
20+
# the diff markers are never inserted). Diff markers may cause the following
21+
# file extensions to fail to load in VS. An alternative would be to treat
22+
# these files as binary and thus will always conflict and require user
23+
# intervention with every merge. To do so, just uncomment the entries below
24+
###############################################################################
25+
#*.sln merge=binary
26+
#*.csproj merge=binary
27+
#*.vbproj merge=binary
28+
#*.vcxproj merge=binary
29+
#*.vcproj merge=binary
30+
#*.dbproj merge=binary
31+
#*.fsproj merge=binary
32+
#*.lsproj merge=binary
33+
#*.wixproj merge=binary
34+
#*.modelproj merge=binary
35+
#*.sqlproj merge=binary
36+
#*.wwaproj merge=binary
37+
38+
###############################################################################
39+
# behavior for image files
40+
#
41+
# image files are treated as binary by default.
42+
###############################################################################
43+
#*.jpg binary
44+
#*.png binary
45+
#*.gif binary
46+
47+
###############################################################################
48+
# diff behavior for common document formats
49+
#
50+
# Convert binary document formats to text before diffing them. This feature
51+
# is only available from the command line. Turn it on by uncommenting the
52+
# entries below.
53+
###############################################################################
54+
#*.doc diff=astextplain
55+
#*.DOC diff=astextplain
56+
#*.docx diff=astextplain
57+
#*.DOCX diff=astextplain
58+
#*.dot diff=astextplain
59+
#*.DOT diff=astextplain
60+
#*.pdf diff=astextplain
61+
#*.PDF diff=astextplain
62+
#*.rtf diff=astextplain
63+
#*.RTF diff=astextplain

‎.gitignore

+454
Large diffs are not rendered by default.

‎KFLP.sln

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
2+
Microsoft Visual Studio Solution File, Format Version 12.00
3+
# Visual Studio Version 17
4+
VisualStudioVersion = 17.6.33801.468
5+
MinimumVisualStudioVersion = 10.0.40219.1
6+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KFLP", "KFLP\KFLP.csproj", "{F352701A-CB1D-41FB-AFE9-D1FD6307DB76}"
7+
EndProject
8+
Global
9+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
10+
Debug|Any CPU = Debug|Any CPU
11+
Release|Any CPU = Release|Any CPU
12+
EndGlobalSection
13+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
14+
{F352701A-CB1D-41FB-AFE9-D1FD6307DB76}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
15+
{F352701A-CB1D-41FB-AFE9-D1FD6307DB76}.Debug|Any CPU.Build.0 = Debug|Any CPU
16+
{F352701A-CB1D-41FB-AFE9-D1FD6307DB76}.Release|Any CPU.ActiveCfg = Release|Any CPU
17+
{F352701A-CB1D-41FB-AFE9-D1FD6307DB76}.Release|Any CPU.Build.0 = Release|Any CPU
18+
EndGlobalSection
19+
GlobalSection(SolutionProperties) = preSolution
20+
HideSolutionNode = FALSE
21+
EndGlobalSection
22+
GlobalSection(ExtensibilityGlobals) = postSolution
23+
SolutionGuid = {3FC5BB25-E9BD-40AE-A053-192D2764EAC2}
24+
EndGlobalSection
25+
EndGlobal

‎KFLP/FLArrangement.cs

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
using Kermalis.EndianBinaryIO;
2+
using System.Collections.Generic;
3+
4+
namespace Kermalis.FLP;
5+
6+
public sealed class FLArrangement
7+
{
8+
internal const int NUM_PLAYLIST_TRACKS = 500;
9+
10+
internal ushort Index;
11+
12+
public string Name;
13+
public readonly List<FLPlaylistItem> PlaylistItems;
14+
public readonly List<FLPlaylistMarker> PlaylistMarkers;
15+
public readonly FLPlaylistTrack[] PlaylistTracks;
16+
17+
public FLArrangement(string name)
18+
{
19+
Name = name;
20+
PlaylistItems = new List<FLPlaylistItem>();
21+
PlaylistMarkers = new List<FLPlaylistMarker>();
22+
23+
PlaylistTracks = new FLPlaylistTrack[NUM_PLAYLIST_TRACKS];
24+
for (ushort i = 0; i < NUM_PLAYLIST_TRACKS; i++)
25+
{
26+
PlaylistTracks[i] = new FLPlaylistTrack(i);
27+
}
28+
}
29+
30+
public void AddToPlaylist(FLPattern p, uint tick, uint duration, FLPlaylistTrack track)
31+
{
32+
PlaylistItems.Add(new FLPlaylistItem(tick, p, duration, track));
33+
}
34+
public void AddToPlaylist(FLAutomation a, uint tick, uint duration, FLPlaylistTrack track)
35+
{
36+
PlaylistItems.Add(new FLPlaylistItem(tick, a, duration, track));
37+
}
38+
public void AddTimeSigMarker(uint tick, byte num, byte denom)
39+
{
40+
PlaylistMarkers.Add(new FLPlaylistMarker(tick, num + "/" + denom, (num, denom)));
41+
}
42+
43+
internal void Write(EndianBinaryWriter w, FLVersionCompat verCom)
44+
{
45+
FLProjectWriter.Write16BitEvent(w, FLEvent.NewArrangement, Index);
46+
FLProjectWriter.WriteUTF16EventWithLength(w, FLEvent.PlaylistArrangementName, Name + '\0');
47+
FLProjectWriter.Write8BitEvent(w, FLEvent.Unk_36, 0);
48+
49+
// Playlist Items. Must be in order of AbsoluteTick
50+
PlaylistItems.Sort((p1, p2) => p1.AbsoluteTick.CompareTo(p2.AbsoluteTick));
51+
52+
w.WriteEnum(FLEvent.PlaylistItems);
53+
FLProjectWriter.WriteArrayEventLength(w, (uint)PlaylistItems.Count * FLPlaylistItem.LEN);
54+
foreach (FLPlaylistItem item in PlaylistItems)
55+
{
56+
item.Write(w);
57+
}
58+
59+
// Playlist Markers
60+
foreach (FLPlaylistMarker mark in PlaylistMarkers)
61+
{
62+
mark.Write(w);
63+
}
64+
65+
// Playlist Tracks
66+
foreach (FLPlaylistTrack track in PlaylistTracks)
67+
{
68+
track.Write(w, verCom);
69+
}
70+
}
71+
}

‎KFLP/FLAutomation.cs

+566
Large diffs are not rendered by default.

‎KFLP/FLAutomation_Point.cs

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
using Kermalis.EndianBinaryIO;
2+
3+
namespace Kermalis.FLP;
4+
5+
partial class FLAutomation
6+
{
7+
public struct Point
8+
{
9+
internal const int LEN = 24;
10+
11+
public uint AbsoluteTicks;
12+
public double Value;
13+
14+
internal readonly void Write(EndianBinaryWriter w, uint ppqn, bool isFirst, bool isLast, uint nextPointAbsoluteTicks)
15+
{
16+
w.WriteDouble(Value);
17+
w.WriteSingle(0f); // Tension
18+
w.WriteUInt32(isFirst ? 0u : 2); // Hold for non-first ones
19+
20+
// Delta ticks in quarter bars
21+
if (isLast)
22+
{
23+
w.WriteUInt64(0xFFFFFFFF00000001); // Special NaN for some reason
24+
}
25+
else
26+
{
27+
// Do Delta ticks for the previous point here.
28+
uint deltaTicks = nextPointAbsoluteTicks - AbsoluteTicks;
29+
w.WriteDouble(deltaTicks / (double)ppqn);
30+
}
31+
}
32+
}
33+
}

‎KFLP/FLBasicChannelParams.cs

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
using Kermalis.EndianBinaryIO;
2+
3+
namespace Kermalis.FLP;
4+
5+
public struct FLBasicChannelParams
6+
{
7+
public const uint KNOB_MIN = 0;
8+
public const uint KNOB_HALF = 6_400;
9+
public const uint KNOB_MAX = 12_800; // 0x3200
10+
public const uint DEFAULT_PAN = KNOB_HALF; // Center
11+
public const uint DEFAULT_VOL = 10_000; // 78.125%
12+
13+
internal static void WriteChannel(EndianBinaryWriter w, uint panKnob, uint volKnob, int pitchKnob)
14+
{
15+
w.WriteEnum(FLEvent.BasicChannelParams);
16+
FLProjectWriter.WriteArrayEventLength(w, 24);
17+
18+
w.WriteUInt32(panKnob);
19+
w.WriteUInt32(volKnob);
20+
w.WriteInt32(pitchKnob); // In cents
21+
22+
w.WriteUInt32(0x100); // Always 0x100?
23+
w.WriteUInt32(0); // Always 0?
24+
w.WriteUInt32(0); // Always 0?
25+
}
26+
internal static void WriteAutomation(EndianBinaryWriter w)
27+
{
28+
w.WriteEnum(FLEvent.BasicChannelParams);
29+
FLProjectWriter.WriteArrayEventLength(w, 24);
30+
31+
w.WriteUInt32(0); // 0% min volume
32+
w.WriteUInt32(12_800); // 100% max volume
33+
w.WriteInt32(0);
34+
35+
w.WriteUInt32(0x100);
36+
w.WriteUInt32(0);
37+
w.WriteUInt32(0);
38+
}
39+
}

‎KFLP/FLChannel.cs

+166
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
using Kermalis.EndianBinaryIO;
2+
using System;
3+
4+
namespace Kermalis.FLP;
5+
6+
public sealed class FLChannel
7+
{
8+
/// <summary>Found in "Miscellaneous functions" of a channel. Automation channels have it too, despite that not being accessible in the GUI</summary>
9+
internal static ReadOnlySpan<byte> Delay => new byte[20]
10+
{
11+
0x00, 0x00, // 0-1: EchoFeed
12+
0x00, 0x00, 0x00, 0x19, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
13+
0x04, // 12: Echoes default=(4)
14+
0x00, 0x00, 0x00,
15+
0x90, 0x00, // 16-17: EchoTime default=(0x90 = 144 => 3:00)
16+
0x00, 0x00
17+
};
18+
internal static ReadOnlySpan<byte> ChanOfsLevels => new byte[20]
19+
{
20+
0x00, 0x00, 0x00, 0x00, // 0
21+
0x00, 0x32, 0x00, 0x00, // 12_800
22+
0x00, 0x00, 0x00, 0x00,
23+
0x00, 0x00, 0x00, 0x00,
24+
0x00, 0x00, 0x00, 0x00
25+
};
26+
private static ReadOnlySpan<byte> ChanPoly => new byte[9]
27+
{
28+
0x00, 0x00, 0x00, 0x00,
29+
0xF4, 0x01, // 500
30+
0x00, 0x00,
31+
0x00
32+
};
33+
// 100
34+
internal static ReadOnlySpan<byte> Tracking0 => new byte[16] { 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
35+
// 60
36+
internal static ReadOnlySpan<byte> Tracking1 => new byte[16] { 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
37+
38+
internal static ReadOnlySpan<byte> EnvelopeOther => new byte[68]
39+
{
40+
0x00,
41+
42+
0x00, 0x00, 0x00,
43+
0x00, 0x00, 0x00, 0x00,
44+
0x64, 0x00, 0x00, 0x00, // 100
45+
0x20, 0x4E, 0x00, 0x00, // 20_000
46+
0x20, 0x4E, 0x00, 0x00, // 20_000
47+
0x30, 0x75, 0x00, 0x00, // 30_000
48+
0x32, 0x00, 0x00, 0x00, // 50
49+
0x20, 0x4E, 0x00, 0x00, // 20_000
50+
0x00, 0x00, 0x00, 0x00,
51+
0x64, 0x00, 0x00, 0x00, // 100
52+
0x20, 0x4E, 0x00, 0x00, // 20_000
53+
0x00, 0x00, 0x00, 0x00,
54+
0xB6, 0x80, 0x00, 0x00, // 32_950
55+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
56+
57+
0x00, 0x00, 0x00, 0x00
58+
};
59+
internal static ReadOnlySpan<byte> Envelope1 => new byte[68]
60+
{
61+
0x04,
62+
63+
0x00, 0x00, 0x00,
64+
0x00, 0x00, 0x00, 0x00,
65+
0x64, 0x00, 0x00, 0x00,
66+
0x20, 0x4E, 0x00, 0x00,
67+
0x20, 0x4E, 0x00, 0x00,
68+
0x30, 0x75, 0x00, 0x00,
69+
0x32, 0x00, 0x00, 0x00,
70+
0x20, 0x4E, 0x00, 0x00,
71+
0x00, 0x00, 0x00, 0x00,
72+
0x64, 0x00, 0x00, 0x00,
73+
0x20, 0x4E, 0x00, 0x00,
74+
0x00, 0x00, 0x00, 0x00,
75+
0xB6, 0x80, 0x00, 0x00,
76+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
77+
78+
0x9B, 0xFF, 0xFF, 0xFF // -101
79+
};
80+
81+
public static FLColor3 DefaultColor => new(92, 101, 106);
82+
public static FLColor3 MIDIOutDefaultColor => new(96, 114, 115);
83+
84+
internal ushort Index;
85+
86+
public string Name;
87+
public FLColor3 Color;
88+
public byte MIDIChannel;
89+
public byte MIDIBank;
90+
public byte MIDIProgram;
91+
public FLChannelFilter Filter;
92+
public uint PanKnob;
93+
public uint VolKnob;
94+
/// <summary>In cents</summary>
95+
public int PitchKnob;
96+
public int PitchBendRange;
97+
98+
internal FLChannel(string name, byte midiChan, byte midiBank, byte midiProgram, FLChannelFilter filter)
99+
{
100+
Name = name;
101+
Color = MIDIOutDefaultColor;
102+
MIDIChannel = midiChan;
103+
MIDIBank = midiBank;
104+
MIDIProgram = midiProgram;
105+
Filter = filter;
106+
PanKnob = FLBasicChannelParams.DEFAULT_PAN;
107+
VolKnob = FLBasicChannelParams.DEFAULT_VOL;
108+
PitchKnob = 0;
109+
PitchBendRange = 2;
110+
}
111+
112+
internal void Write(EndianBinaryWriter w, FLVersionCompat verCom)
113+
{
114+
FLProjectWriter.Write16BitEvent(w, FLEvent.NewChannel, Index);
115+
FLProjectWriter.Write8BitEvent(w, FLEvent.ChannelType, (byte)FLChannelType.FLPlugin);
116+
117+
FLProjectWriter.WriteUTF16EventWithLength(w, FLEvent.DefPluginName, "MIDI Out\0");
118+
FLNewPlugin.WriteMIDIOut(w);
119+
FLProjectWriter.WriteUTF16EventWithLength(w, FLEvent.PluginName, Name + '\0');
120+
FLProjectWriter.Write32BitEvent(w, FLEvent.PluginIcon, 0);
121+
FLProjectWriter.Write32BitEvent(w, FLEvent.PluginColor, Color.GetFLValue());
122+
if (verCom == FLVersionCompat.V21_0_3__B3517)
123+
{
124+
// Always 1 with MIDIOut even if you use the theme's suggested default colors
125+
// Sampler is 0 though if you use suggested
126+
FLProjectWriter.Write8BitEvent(w, FLEvent.PluginIgnoresTheme, 1);
127+
}
128+
FLPluginParams.WriteMIDIOut(w, MIDIChannel, MIDIBank, MIDIProgram);
129+
130+
FLProjectWriter.Write8BitEvent(w, FLEvent.ChannelIsEnabled, 1);
131+
FLProjectWriter.WriteArrayEventWithLength(w, FLEvent.Delay, Delay);
132+
FLProjectWriter.Write32BitEvent(w, FLEvent.DelayReso, 0x800_080);
133+
FLProjectWriter.Write32BitEvent(w, FLEvent.Reverb, 0x10_000);
134+
FLProjectWriter.Write16BitEvent(w, FLEvent.ShiftDelay, 0);
135+
FLProjectWriter.Write16BitEvent(w, FLEvent.SwingMix, 0x80);
136+
FLProjectWriter.Write16BitEvent(w, FLEvent.FX, 0x80);
137+
FLProjectWriter.Write16BitEvent(w, FLEvent.FX3, 0x100);
138+
FLProjectWriter.Write16BitEvent(w, FLEvent.CutOff, 0x400);
139+
FLProjectWriter.Write16BitEvent(w, FLEvent.Resonance, 0);
140+
FLProjectWriter.Write16BitEvent(w, FLEvent.PreAmp, 0);
141+
FLProjectWriter.Write16BitEvent(w, FLEvent.Decay, 0);
142+
FLProjectWriter.Write16BitEvent(w, FLEvent.Attack, 0);
143+
FLProjectWriter.Write16BitEvent(w, FLEvent.StDel, 0x800);
144+
FLProjectWriter.Write32BitEvent(w, FLEvent.FXSine, 0x800_000);
145+
FLProjectWriter.Write16BitEvent(w, FLEvent.Fade_Stereo, (ushort)FLFadeStereo.None);
146+
FLProjectWriter.Write8BitEvent(w, FLEvent.TargetFXTrack, 0);
147+
FLBasicChannelParams.WriteChannel(w, PanKnob, VolKnob, PitchKnob);
148+
FLProjectWriter.WriteArrayEventWithLength(w, FLEvent.ChanOfsLevels, ChanOfsLevels);
149+
FLProjectWriter.WriteArrayEventWithLength(w, FLEvent.ChanPoly, ChanPoly);
150+
FLChannelParams.WriteMIDIOut(w, Index, PitchBendRange);
151+
FLProjectWriter.Write32BitEvent(w, FLEvent.CutCutBy, (uint)(Index + 1) * 0x10_001u);
152+
FLProjectWriter.Write32BitEvent(w, FLEvent.ChannelLayerFlags, 0);
153+
FLProjectWriter.Write32BitEvent(w, FLEvent.ChanFilterNum, Filter.Index);
154+
//
155+
FLProjectWriter.Write8BitEvent(w, FLEvent.Unk_32, 0);
156+
FLProjectWriter.WriteArrayEventWithLength(w, FLEvent.ChannelTracking, Tracking0);
157+
FLProjectWriter.WriteArrayEventWithLength(w, FLEvent.ChannelTracking, Tracking1);
158+
FLProjectWriter.WriteArrayEventWithLength(w, FLEvent.ChannelEnvelope, EnvelopeOther);
159+
FLProjectWriter.WriteArrayEventWithLength(w, FLEvent.ChannelEnvelope, Envelope1);
160+
FLProjectWriter.WriteArrayEventWithLength(w, FLEvent.ChannelEnvelope, EnvelopeOther);
161+
FLProjectWriter.WriteArrayEventWithLength(w, FLEvent.ChannelEnvelope, EnvelopeOther);
162+
FLProjectWriter.WriteArrayEventWithLength(w, FLEvent.ChannelEnvelope, EnvelopeOther);
163+
FLProjectWriter.Write32BitEvent(w, FLEvent.ChannelSampleFlags, 0b1010);
164+
FLProjectWriter.Write8BitEvent(w, FLEvent.ChannelLoopType, 0);
165+
}
166+
}

‎KFLP/FLChannelFilter.cs

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
using Kermalis.EndianBinaryIO;
2+
3+
namespace Kermalis.FLP;
4+
5+
public sealed class FLChannelFilter
6+
{
7+
internal ushort Index;
8+
9+
public string Name;
10+
11+
internal FLChannelFilter(string name)
12+
{
13+
Name = name;
14+
}
15+
16+
internal void Write(EndianBinaryWriter w)
17+
{
18+
FLProjectWriter.WriteUTF16EventWithLength(w, FLEvent.ChanFilterName, Name + '\0');
19+
}
20+
}

‎KFLP/FLChannelParams.cs

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
using Kermalis.EndianBinaryIO;
2+
3+
namespace Kermalis.FLP;
4+
5+
internal struct FLChannelParams
6+
{
7+
public static void WriteMIDIOut(EndianBinaryWriter w, ushort chanIndex, int pitchBendRange)
8+
{
9+
Write(w, 1, (byte)chanIndex, pitchBendRange);
10+
}
11+
public static void WriteAutomation(EndianBinaryWriter w, int pitchBendOrTimeRange)
12+
{
13+
Write(w, 0, 1, pitchBendOrTimeRange);
14+
}
15+
16+
private static void Write(EndianBinaryWriter w, byte typeProbably, byte someID, int pitchBendRange)
17+
{
18+
w.WriteEnum(FLEvent.ChannelParams);
19+
FLProjectWriter.WriteArrayEventLength(w, 168);
20+
21+
w.WriteInt32(-1);
22+
w.WriteInt32(0);
23+
24+
w.WriteUInt16(1);
25+
w.WriteByte(0);
26+
w.WriteByte(typeProbably);
27+
28+
w.WriteInt32(-1);
29+
w.WriteUInt32(60); // Root Key: C5 => 60
30+
w.WriteSingle(1f);
31+
w.WriteSingle(1f);
32+
w.WriteSingle(1f);
33+
w.WriteSingle(1f);
34+
w.WriteSingle(1f);
35+
w.WriteInt32((int)ArpDirection.Off); // Arp Direction
36+
w.WriteInt32(1); // Arp Range
37+
w.WriteInt32(-1); // Arp Chord: Major => 1 | Autosustain => -1
38+
w.WriteUInt32(0x400); // Arp Time: 0:03 => 0x1C2 (450) | 1:00 => 0x400 (1024) | 4:00 => 0x5A6 (1446) | Hold => 0x5A7 (1447)
39+
w.WriteUInt32(48); // Arp Gate: 0% => 0, 100% => 48
40+
41+
w.WriteByte(0); // Arp Slide
42+
w.WriteByte(0);
43+
w.WriteByte(0); // Was 0 for a default sampler
44+
w.WriteByte(0);
45+
46+
w.WriteUInt32(0x5A7); // ? 1447 like Arp Time above
47+
w.WriteInt32(0);
48+
w.WriteUInt32(0x100);
49+
w.WriteInt32(0);
50+
w.WriteInt32(0);
51+
w.WriteInt32(0);
52+
w.WriteInt32(0);
53+
w.WriteInt32(1); // Arp Repeat
54+
w.WriteInt32(0);
55+
w.WriteInt32(0);
56+
w.WriteInt32(0);
57+
w.WriteInt32(0);
58+
w.WriteInt32(pitchBendRange);
59+
w.WriteInt32(-2);
60+
w.WriteInt32(-1);
61+
w.WriteInt32(0);
62+
w.WriteDouble(0d);
63+
w.WriteDouble(1d);
64+
w.WriteDouble(0d);
65+
w.WriteInt32(-1);
66+
67+
w.WriteByte(someID); // TODO: What if the channel ID is larger than 255?
68+
w.WriteByte(1);
69+
w.WriteUInt16(0);
70+
71+
w.WriteDouble(0.5d);
72+
}
73+
}

‎KFLP/FLColor3.cs

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
using System;
2+
3+
namespace Kermalis.FLP;
4+
5+
public struct FLColor3
6+
{
7+
public byte R;
8+
public byte G;
9+
public byte B;
10+
11+
/// <summary>This constructor takes <see cref="R"/> from the LSB and <see cref="B"/> from the MSB</summary>
12+
public FLColor3(uint flValue)
13+
{
14+
R = (byte)(flValue & 0xFF);
15+
G = (byte)((flValue >> 8) & 0xFF);
16+
B = (byte)((flValue >> 16) & 0xFF);
17+
}
18+
public FLColor3(byte r, byte g, byte b)
19+
{
20+
R = r;
21+
G = g;
22+
B = b;
23+
}
24+
25+
public static FLColor3 GetRandom()
26+
{
27+
return new FLColor3((uint)Random.Shared.Next(0x1_000_000));
28+
}
29+
public static FLColor3 FromRGB(uint rgb)
30+
{
31+
byte b = (byte)(rgb & 0xFF);
32+
byte g = (byte)((rgb >> 8) & 0xFF);
33+
byte r = (byte)((rgb >> 16) & 0xFF);
34+
return new FLColor3(r, g, b);
35+
}
36+
37+
public readonly uint GetFLValue()
38+
{
39+
return R | ((uint)G << 8) | ((uint)B << 16);
40+
}
41+
42+
public readonly bool Equals(FLColor3 other)
43+
{
44+
return R == other.R && G == other.G && B == other.B;
45+
}
46+
47+
public override readonly string ToString()
48+
{
49+
return string.Format("R {0} G {1} B {2}", R, G, B);
50+
}
51+
}

‎KFLP/FLEnums.cs

+99
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
using System;
2+
3+
namespace Kermalis.FLP;
4+
5+
public enum FLVersionCompat : byte
6+
{
7+
V20_9_2__B2963,
8+
V21_0_3__B3517,
9+
}
10+
11+
internal enum FLChannelType : byte
12+
{
13+
Sampler,
14+
/// <summary>Doesn't exist past FL12</summary>
15+
TS404,
16+
FLPlugin,
17+
Layer,
18+
// 4
19+
Automation = 5,
20+
}
21+
22+
[Flags]
23+
internal enum FLFadeStereo : ushort
24+
{
25+
None = 0,
26+
Unk_0 = 1 << 0,
27+
SampleReversed = 1 << 1,
28+
Unk_2 = 1 << 2,
29+
Unk_3 = 1 << 3,
30+
Unk_4 = 1 << 4,
31+
Unk_5 = 1 << 5,
32+
Unk_6 = 1 << 6,
33+
Unk_7 = 1 << 7,
34+
SampleReverseStereo = 1 << 8,
35+
Unk_9 = 1 << 9,
36+
Unk_10 = 1 << 10,
37+
Unk_11 = 1 << 11,
38+
Unk_12 = 1 << 12,
39+
Unk_13 = 1 << 13,
40+
Unk_14 = 1 << 14,
41+
Unk_15 = 1 << 15,
42+
}
43+
44+
internal enum FLMixerParamsEvent : byte
45+
{
46+
SlotState = 0x0,
47+
SlotVolume = 0x1,
48+
SlotDryWet = 0x2,
49+
Unk_A4 = 0xA4,
50+
Unk_A5 = 0xA5,
51+
Unk_A6 = 0xA6,
52+
Unk_A7 = 0xA7,
53+
Unk_A8 = 0xA8,
54+
Unk_BE = 0xBE,
55+
Volume = 0xC0,
56+
Pan = 0xC1,
57+
StereoSeparation = 0xC2,
58+
LowLevel = 0xD0,
59+
BandLevel = 0xD1,
60+
HighLevel = 0xD2,
61+
LowFreq = 0xD8,
62+
BandFreq = 0xD9,
63+
HighFreq = 0xDA,
64+
LowWidth = 0xE0,
65+
BandWidth = 0xE1,
66+
HighWidth = 0xE2,
67+
}
68+
69+
[Flags]
70+
internal enum InsertFlags : ushort
71+
{
72+
None = 0,
73+
ReversePolarity = 1 << 0,
74+
SwapChannels = 1 << 1,
75+
Unk_2 = 1 << 2,
76+
Unmuted = 1 << 3,
77+
DisableThreaded = 1 << 4,
78+
Unk_5 = 1 << 5,
79+
DockMiddle = 1 << 6,
80+
DockRight = 1 << 7,
81+
Unk_8 = 1 << 8,
82+
Unk_9 = 1 << 9,
83+
Separator = 1 << 10,
84+
Lock = 1 << 11,
85+
Solo = 1 << 12,
86+
Unk_13 = 1 << 13,
87+
Unk_14 = 1 << 14,
88+
Unk_15 = 1 << 15,
89+
}
90+
91+
internal enum ArpDirection : byte
92+
{
93+
Off = 0,
94+
Up = 1,
95+
Down = 2,
96+
UpDownBounce = 3,
97+
UpDownSticky = 4,
98+
Random = 5,
99+
}

‎KFLP/FLEvent.cs

+207
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
namespace Kermalis.FLP;
2+
3+
internal enum FLEvent : byte
4+
{
5+
// 8bit
6+
ChannelIsEnabled = 0x00,
7+
NoteOn,
8+
ChannelVolume,
9+
ChannelPanpot,
10+
MIDIChan,
11+
MIDINote,
12+
MIDIPatch,
13+
MIDIBank,
14+
// 8
15+
/// <summary>0 for pattern mode, 1 for song mode</summary>
16+
IsSongMode = 9,
17+
/// <summary>0 or 1</summary>
18+
ShouldShowInfoOnOpen,
19+
Shuffle,
20+
MainVolume,
21+
FitToSteps,
22+
Pitchable,
23+
Zipped,
24+
DelayFlags,
25+
ProjectTimeSigNumerator,
26+
ProjectTimeSigDenominator,
27+
UseLoopPoints,
28+
ChannelLoopType,
29+
/// <summary><see cref="FLChannelType"/></summary>
30+
ChannelType,
31+
TargetFXTrack,
32+
/// <summary>0 for circular, 2 for triangular</summary>
33+
PanningLaw,
34+
NStepsShown,
35+
SSLength,
36+
SSLoop,
37+
FXProps,
38+
IsRegistered,
39+
APDC,
40+
ShouldPlayTruncatedClipNotes,
41+
EEAutoMode,
42+
Unk_32, // 0
43+
TimeSigMarkerNumerator,
44+
TimeSigMarkerDenominator,
45+
/// <summary>0 for original FL timing, 1 for traditional time signatures</summary>
46+
ProjectShouldUseTimeSignatures,
47+
Unk_36, // 0
48+
Unk_37, // 1
49+
Unk_38, // 1
50+
Unk_39, // 0
51+
ShouldCutNotesFast,
52+
/// <summary>FL21</summary>
53+
PluginIgnoresTheme,
54+
/// <summary>FL21</summary>
55+
InsertIgnoresTheme,
56+
/// <summary>FL21</summary>
57+
PlaylistTrackIgnoresTheme,
58+
/// <summary>FL21</summary>
59+
PlaylistShouldUseAutoCrossfades,
60+
61+
// 16bit
62+
NewChannel = 0x40,
63+
NewPattern,
64+
Tempo,
65+
SelectedPatternNum,
66+
PatData,
67+
FX,
68+
/// <summary><see cref="FLFadeStereo"/></summary>
69+
Fade_Stereo,
70+
CutOff,
71+
DotVol,
72+
DotPan,
73+
PreAmp,
74+
Decay,
75+
Attack,
76+
DotNote,
77+
DotPitch,
78+
DotMix,
79+
/// <summary>-1200 to 1200. 0 default</summary>
80+
MasterPitch,
81+
RandChan,
82+
MixChan,
83+
Resonance,
84+
OldSongLoopPos,
85+
StDel,
86+
FX3,
87+
DotReso,
88+
DotCutOff,
89+
ShiftDelay,
90+
LoopEndBar,
91+
Dot,
92+
DotShift,
93+
TempoFine,
94+
LayerChan,
95+
InsertIcon,
96+
DotRel,
97+
SwingMix,
98+
NewInsertSlot, // 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
99+
NewArrangement,
100+
CurArrangementNum,
101+
102+
// 32bit
103+
PluginColor = 0x80,
104+
PlaylistItem,
105+
Echo,
106+
FXSine,
107+
/// <summary>Mixture of the "Cut" and "Cut By" numerics on a channel</summary>
108+
CutCutBy,
109+
WindowH,
110+
// 134
111+
MiddleNote = 135,
112+
/// <summary>May contain an invalid version info</summary>
113+
Reserved,
114+
MainResoCutOff,
115+
DelayReso,
116+
Reverb,
117+
StretchTime,
118+
SSNote,
119+
FineTune,
120+
ChannelSampleFlags,
121+
ChannelLayerFlags,
122+
ChanFilterNum,
123+
CurFilterNum,
124+
/// <summary>Insert track output channel - -1 for none</summary>
125+
InsertOutChanNum,
126+
/// <summary>+ Time and Mode in higher bits</summary>
127+
NewTimeMarker,
128+
InsertColor,
129+
PatternColor,
130+
PatternAutoMode,
131+
SongLoopPos,
132+
AUSmpRate,
133+
/// <summary>Insert track input channel - -1 for none</summary>
134+
InsertInChanNum,
135+
PluginIcon,
136+
FineTempo,
137+
Unk_157, // -1
138+
Unk_158, // -1
139+
VersionBuildNumber,
140+
Unk_164 = 164, // 0
141+
Unk_165, // 3
142+
Unk_166, // 1
143+
144+
// array
145+
ChannelName = 0xC0,
146+
PatternName,
147+
ProjectTitle,
148+
ProjectComment,
149+
/// <summary>Filename for the sample in the current channel, stored as relative path</summary>
150+
SampleFileName,
151+
ProjectURL,
152+
/// <summary>New comments in Rich Text format</summary>
153+
ProjectCommentRTF,
154+
Version,
155+
RegistrationID,
156+
/// <summary>Plugin file name (without path)</summary>
157+
DefPluginName,
158+
ProjectDataPath,
159+
/// <summary>Plugin's name</summary>
160+
PluginName,
161+
InsertName,
162+
TimeMarkerName,
163+
ProjectGenre,
164+
ProjectAuthor,
165+
MIDICtrls,
166+
Delay,
167+
TS404Params,
168+
DelayLine,
169+
NewPlugin,
170+
PluginParams,
171+
/// <summary>Used once for testing</summary>
172+
Reserved2,
173+
ChannelParams,
174+
/// <summary>Automated controller events</summary>
175+
CtrlRecChan,
176+
/// <summary>Selection in playlist</summary>
177+
PlaylistSelection,
178+
ChannelEnvelope,
179+
BasicChannelParams,
180+
OldFilterParams,
181+
ChanPoly,
182+
NoteEvents,
183+
PatternEvents,
184+
PatternNotes,
185+
MixerParams,
186+
MIDIInfo,
187+
AutomationConnection,
188+
/// <summary>Vol/kb tracking</summary>
189+
ChannelTracking,
190+
/// <summary>Levels offset</summary>
191+
ChanOfsLevels,
192+
/// <summary>Remote control entry formula</summary>
193+
RemoteCtrlFormula,
194+
ChanFilterName,
195+
RegBlackList,
196+
PlaylistItems,
197+
AutomationData,
198+
InsertRouting,
199+
InsertParams,
200+
/// <summary>Value like: 10 DF D7 ED 3B A4 E5 40 00 00 00 E0 C9 BE 32 3F</summary>
201+
ProjectTime,
202+
NewPlaylistTrack,
203+
PlaylistTrackName,
204+
PlaylistArrangementName = 241,
205+
206+
MAX
207+
}

‎KFLP/FLInsert.cs

+130
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
using Kermalis.EndianBinaryIO;
2+
using System;
3+
4+
namespace Kermalis.FLP;
5+
6+
public sealed class FLInsert
7+
{
8+
public sealed class FLFruityLSDOptions
9+
{
10+
public byte MIDIBank;
11+
/// <summary>UTF8</summary>
12+
public string DLSPath;
13+
public uint Icon;
14+
public FLColor3 Color;
15+
16+
public FLFruityLSDOptions(byte midiBank, string dlsPath, FLColor3 color)
17+
{
18+
MIDIBank = midiBank;
19+
DLSPath = dlsPath;
20+
Color = color;
21+
}
22+
23+
public static FLColor3 GetDefaultColor(FLVersionCompat verCom)
24+
{
25+
switch (verCom)
26+
{
27+
case FLVersionCompat.V20_9_2__B2963: return new FLColor3(72, 81, 86);
28+
case FLVersionCompat.V21_0_3__B3517: return new FLColor3(92, 101, 106);
29+
}
30+
throw new ArgumentOutOfRangeException(nameof(verCom), verCom, null);
31+
}
32+
33+
internal void Write(EndianBinaryWriter w, FLVersionCompat verCom, byte insertIndex)
34+
{
35+
FLProjectWriter.WriteUTF16EventWithLength(w, FLEvent.DefPluginName, "Fruity LSD\0");
36+
FLNewPlugin.WriteFruityLSD(w, insertIndex);
37+
FLProjectWriter.Write32BitEvent(w, FLEvent.PluginIcon, Icon);
38+
FLProjectWriter.Write32BitEvent(w, FLEvent.PluginColor, Color.GetFLValue());
39+
if (verCom == FLVersionCompat.V21_0_3__B3517)
40+
{
41+
byte val = (byte)(Color.Equals(GetDefaultColor(verCom)) ? 0 : 1);
42+
FLProjectWriter.Write8BitEvent(w, FLEvent.PluginIgnoresTheme, val);
43+
}
44+
FLPluginParams.WriteFruityLSD(w, MIDIBank, DLSPath);
45+
}
46+
}
47+
48+
public static FLColor3 DefaultColor => new(99, 108, 113);
49+
50+
private readonly byte _index;
51+
public FLFruityLSDOptions? FruityLSD;
52+
public FLColor3 Color;
53+
public ushort? Icon;
54+
public string? Name;
55+
56+
internal FLInsert(byte index)
57+
{
58+
_index = index;
59+
Color = DefaultColor;
60+
}
61+
62+
internal void Write(EndianBinaryWriter w, FLVersionCompat verCom)
63+
{
64+
// Color/Icon/Name can exist independently of each other unlike patterns
65+
bool isDefaultColor = Color.Equals(DefaultColor);
66+
if (!isDefaultColor)
67+
{
68+
FLProjectWriter.Write32BitEvent(w, FLEvent.InsertColor, Color.GetFLValue());
69+
}
70+
// If color is present, it goes above this
71+
if (verCom == FLVersionCompat.V21_0_3__B3517)
72+
{
73+
byte val = (byte)(isDefaultColor ? 0 : 1);
74+
FLProjectWriter.Write8BitEvent(w, FLEvent.InsertIgnoresTheme, val);
75+
}
76+
if (Icon is not null)
77+
{
78+
FLProjectWriter.Write16BitEvent(w, FLEvent.InsertIcon, Icon.Value);
79+
}
80+
if (Name is not null)
81+
{
82+
FLProjectWriter.WriteUTF16EventWithLength(w, FLEvent.InsertName, Name + '\0');
83+
}
84+
85+
bool isMasterOrCurrent = _index is 0 or 126;
86+
FLInsertParams.Write(w, isMasterOrCurrent);
87+
88+
FruityLSD?.Write(w, verCom, _index);
89+
90+
FLProjectWriter.Write16BitEvent(w, FLEvent.NewInsertSlot, 0);
91+
FLProjectWriter.Write16BitEvent(w, FLEvent.NewInsertSlot, 1);
92+
FLProjectWriter.Write16BitEvent(w, FLEvent.NewInsertSlot, 2);
93+
FLProjectWriter.Write16BitEvent(w, FLEvent.NewInsertSlot, 3);
94+
FLProjectWriter.Write16BitEvent(w, FLEvent.NewInsertSlot, 4);
95+
FLProjectWriter.Write16BitEvent(w, FLEvent.NewInsertSlot, 5);
96+
FLProjectWriter.Write16BitEvent(w, FLEvent.NewInsertSlot, 6);
97+
FLProjectWriter.Write16BitEvent(w, FLEvent.NewInsertSlot, 7);
98+
FLProjectWriter.Write16BitEvent(w, FLEvent.NewInsertSlot, 8);
99+
FLProjectWriter.Write16BitEvent(w, FLEvent.NewInsertSlot, 9);
100+
if (isMasterOrCurrent)
101+
{
102+
WriteRouting_None(w);
103+
}
104+
else
105+
{
106+
WriteRouting_GoToMaster(w);
107+
}
108+
FLProjectWriter.Write32BitEvent(w, FLEvent.Unk_165, 3);
109+
FLProjectWriter.Write32BitEvent(w, FLEvent.Unk_166, 1);
110+
FLProjectWriter.Write32BitEvent(w, FLEvent.InsertInChanNum, uint.MaxValue);
111+
FLProjectWriter.Write32BitEvent(w, FLEvent.InsertOutChanNum, _index == 0 ? 0 : uint.MaxValue);
112+
}
113+
private static void WriteRouting_None(EndianBinaryWriter w)
114+
{
115+
w.WriteEnum(FLEvent.InsertRouting);
116+
FLProjectWriter.WriteArrayEventLength(w, 127);
117+
118+
// bool[127]. Go to nothing
119+
w.WriteZeroes(127);
120+
}
121+
private static void WriteRouting_GoToMaster(EndianBinaryWriter w)
122+
{
123+
w.WriteEnum(FLEvent.InsertRouting);
124+
FLProjectWriter.WriteArrayEventLength(w, 127);
125+
126+
// bool[127]. Go to master and nothing else
127+
w.WriteByte(1);
128+
w.WriteZeroes(126);
129+
}
130+
}

‎KFLP/FLInsertParams.cs

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
using Kermalis.EndianBinaryIO;
2+
3+
namespace Kermalis.FLP;
4+
5+
internal struct FLInsertParams
6+
{
7+
private const InsertFlags MASTER_CURRENT_FLAGS = InsertFlags.Unk_2 | InsertFlags.Unmuted;
8+
private const InsertFlags INSERT_FLAGS = InsertFlags.Unk_2 | InsertFlags.Unmuted | InsertFlags.DockMiddle;
9+
10+
public static void Write(EndianBinaryWriter w, bool isMasterOrCurrent)
11+
{
12+
w.WriteEnum(FLEvent.InsertParams);
13+
FLProjectWriter.WriteArrayEventLength(w, 12);
14+
15+
w.WriteUInt32(0); // Always 0?
16+
w.WriteEnum(isMasterOrCurrent ? MASTER_CURRENT_FLAGS : INSERT_FLAGS);
17+
w.WriteUInt16(0); // Always 0?
18+
w.WriteUInt32(0); // Always 0?
19+
}
20+
}

‎KFLP/FLMixerParams.cs

+135
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
using Kermalis.EndianBinaryIO;
2+
using System.IO;
3+
using System.Text;
4+
5+
namespace Kermalis.FLP;
6+
7+
internal static class FLMixerParams
8+
{
9+
private const int BYTES_PER_EVENT = 0xC;
10+
// There are 4697 events:
11+
// 1 = Master Volume event
12+
// 100*38 [3800] = Insert 0-99 events (includes Unk_A4, Unk_A5, Unk_A6, Unk_A7, Unk_A8, Unk_BE)
13+
// 5*34 [170] = Insert 100-104 events (includes Unk_A8, Unk_BE)
14+
// 22*33 [726] = Insert 105-126 events (includes Unk_BE)
15+
private const int NUM_EVENTS =
16+
1
17+
+ (100 * 38)
18+
+ (5 * 34)
19+
+ (22 * 33);
20+
private const int LEN = NUM_EVENTS * BYTES_PER_EVENT;
21+
22+
public static void Write(EndianBinaryWriter w)
23+
{
24+
w.WriteEnum(FLEvent.MixerParams);
25+
FLProjectWriter.WriteArrayEventLength(w, LEN);
26+
27+
WriteMasterVolumeEvent(w, 0x3200);
28+
29+
for (byte insertID = 0; insertID < 127; insertID++)
30+
{
31+
for (byte slotID = 0; slotID < 10; slotID++)
32+
{
33+
WriteInsertSlotEvent(w, FLMixerParamsEvent.SlotState, insertID, slotID, 1);
34+
WriteInsertSlotEvent(w, FLMixerParamsEvent.SlotVolume, insertID, slotID, 0x3200);
35+
}
36+
WriteInsertEvent(w, FLMixerParamsEvent.Volume, insertID, 0x3200);
37+
WriteInsertEvent(w, FLMixerParamsEvent.Pan, insertID, 0);
38+
WriteInsertEvent(w, FLMixerParamsEvent.StereoSeparation, insertID, 0);
39+
WriteInsertEvent(w, FLMixerParamsEvent.LowLevel, insertID, 0);
40+
WriteInsertEvent(w, FLMixerParamsEvent.BandLevel, insertID, 0);
41+
WriteInsertEvent(w, FLMixerParamsEvent.HighLevel, insertID, 0);
42+
WriteInsertEvent(w, FLMixerParamsEvent.LowFreq, insertID, 0x1691);
43+
WriteInsertEvent(w, FLMixerParamsEvent.BandFreq, insertID, 0x8179);
44+
WriteInsertEvent(w, FLMixerParamsEvent.HighFreq, insertID, 0xDA11);
45+
WriteInsertEvent(w, FLMixerParamsEvent.LowWidth, insertID, 0x445C);
46+
WriteInsertEvent(w, FLMixerParamsEvent.BandWidth, insertID, 0x445C);
47+
WriteInsertEvent(w, FLMixerParamsEvent.HighWidth, insertID, 0x445C);
48+
49+
if (insertID < 100)
50+
{
51+
WriteInsertEvent(w, FLMixerParamsEvent.Unk_A4, insertID, 0);
52+
WriteInsertEvent(w, FLMixerParamsEvent.Unk_A5, insertID, 0);
53+
WriteInsertEvent(w, FLMixerParamsEvent.Unk_A6, insertID, 0);
54+
WriteInsertEvent(w, FLMixerParamsEvent.Unk_A7, insertID, 0);
55+
}
56+
if (insertID < 105)
57+
{
58+
WriteInsertEvent(w, FLMixerParamsEvent.Unk_A8, insertID, 0);
59+
}
60+
WriteInsertEvent(w, FLMixerParamsEvent.Unk_BE, insertID, 0);
61+
}
62+
}
63+
private static void WriteMasterVolumeEvent(EndianBinaryWriter w, uint vol)
64+
{
65+
WriteEvent(w, 0, 2, 0, 0, vol);
66+
}
67+
private static void WriteInsertSlotEvent(EndianBinaryWriter w, FLMixerParamsEvent eType, byte insertID, byte slotID, uint eData)
68+
{
69+
WriteEvent(w, eType, 1, insertID, slotID, eData);
70+
}
71+
private static void WriteInsertEvent(EndianBinaryWriter w, FLMixerParamsEvent eType, byte insertID, uint eData)
72+
{
73+
WriteEvent(w, eType, 1, insertID, 0, eData);
74+
}
75+
private static void WriteEvent(EndianBinaryWriter w, FLMixerParamsEvent eType, byte insertType, byte insertID, byte slotID, uint eData)
76+
{
77+
w.WriteUInt32(0);
78+
w.WriteEnum(eType);
79+
w.WriteByte(0x1F);
80+
w.WriteUInt16((ushort)(slotID | (insertID << 6) | (insertType << 13)));
81+
w.WriteUInt32(eData);
82+
}
83+
84+
public static string ReadData(byte[] bytes)
85+
{
86+
if (bytes.Length != LEN)
87+
{
88+
throw new InvalidDataException("Unexpected MixerParams length: " + bytes.Length);
89+
}
90+
91+
using (var ms = new MemoryStream(bytes))
92+
{
93+
var r = new EndianBinaryReader(ms);
94+
var str = new StringBuilder();
95+
96+
str.AppendLine("{");
97+
98+
while (ms.Position < ms.Length)
99+
{
100+
long startPos = ms.Position;
101+
102+
uint unk0 = r.ReadUInt32(); // Always 0?
103+
var eType = (FLMixerParamsEvent)r.ReadByte();
104+
byte unk5 = r.ReadByte(); // Always 0x1F (31) [0b0001_1111]?
105+
ushort eFlags = r.ReadUInt16();
106+
uint eData = r.ReadUInt32();
107+
108+
// eFlags bits: [ttti iiii iiss ssss]
109+
int slotId = eFlags & 0x3F; // s: [0, 63]
110+
int insertId = (eFlags >> 6) & 0x7F; // i: [0, 127]
111+
int insertType = eFlags >> 13; // t: [0, 7]
112+
113+
str.Append($"t {insertType} @ 0x{startPos:X4} => ");
114+
115+
if (insertType == 2)
116+
{
117+
str.Append($"Master Volume = 0x{eData:X}");
118+
}
119+
else if (insertType == 1)
120+
{
121+
str.Append($"Insert #{insertId} slot #{slotId} {eType} = 0x{eData:X}");
122+
}
123+
else
124+
{
125+
str.Append($"Unknown: i={insertId} s={slotId} eType={eType}, eData=0x{eData:X8}");
126+
}
127+
128+
str.AppendLine($" (unk0=0x{unk0}, unk5=0x{unk5:X})");
129+
}
130+
131+
str.Append('}');
132+
return str.ToString();
133+
}
134+
}
135+
}

‎KFLP/FLNewPlugin.cs

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
using Kermalis.EndianBinaryIO;
2+
3+
namespace Kermalis.FLP;
4+
5+
internal struct FLNewPlugin
6+
{
7+
// VST Notes: https://github.com/Kaydax/FLParser/blob/2f48809bbf8f31c4ecf93051f5f1fa86a84b5468/ProjectParser.cs#L601
8+
9+
public static void WriteMIDIOut(EndianBinaryWriter w)
10+
{
11+
w.WriteEnum(FLEvent.NewPlugin);
12+
FLProjectWriter.WriteArrayEventLength(w, 52);
13+
14+
w.WriteUInt32(0);
15+
w.WriteUInt32(0);
16+
w.WriteUInt32(2);
17+
w.WriteUInt32(0);
18+
w.WriteUInt32(0b1_0101_0100); // 0x154 (340) = That 1 at the left seems to be "is open"? But these are closed...
19+
w.WriteUInt32(0);
20+
w.WriteUInt32(0);
21+
w.WriteUInt32(0);
22+
w.WriteUInt32(0);
23+
w.WriteUInt32(4); // Selection/pos related. Was 0x0112 when selected, then 0x0004 when deselected in the same pos. Also saw it 0x12B
24+
w.WriteUInt32(4); // Saw 0x15D
25+
w.WriteUInt32(0);
26+
w.WriteUInt32(0);
27+
}
28+
public static void WriteAutomation(EndianBinaryWriter w)
29+
{
30+
w.WriteEnum(FLEvent.NewPlugin);
31+
FLProjectWriter.WriteArrayEventLength(w, 52);
32+
33+
w.WriteUInt32(0);
34+
w.WriteUInt32(0);
35+
w.WriteInt32(-1);
36+
w.WriteUInt32(0);
37+
w.WriteUInt32(0b0_0101_0100); // 0x54 (84)
38+
w.WriteUInt32(5);
39+
w.WriteUInt32(0);
40+
w.WriteUInt32(0);
41+
w.WriteUInt32(0);
42+
w.WriteUInt32(6);
43+
w.WriteUInt32(6);
44+
w.WriteUInt32(0);
45+
w.WriteUInt32(0);
46+
}
47+
public static void WriteFruityLSD(EndianBinaryWriter w, byte insertIndex)
48+
{
49+
w.WriteEnum(FLEvent.NewPlugin);
50+
FLProjectWriter.WriteArrayEventLength(w, 52);
51+
52+
w.WriteUInt32(insertIndex);
53+
w.WriteUInt32(0);
54+
w.WriteUInt32(2);
55+
w.WriteUInt32(0);
56+
w.WriteUInt32(0b1_0100_0100); // 0x144 (324)
57+
w.WriteUInt32(0);
58+
w.WriteUInt32(0);
59+
w.WriteUInt32(0);
60+
w.WriteUInt32(0);
61+
w.WriteUInt32(0x3D3); // 979
62+
w.WriteUInt32(0x337); // 823
63+
w.WriteUInt32(0);
64+
w.WriteUInt32(0);
65+
}
66+
}

‎KFLP/FLPattern.cs

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
using Kermalis.EndianBinaryIO;
2+
using System.Collections.Generic;
3+
4+
namespace Kermalis.FLP;
5+
6+
public sealed class FLPattern
7+
{
8+
public static FLColor3 DefaultColor => new(72, 81, 86);
9+
10+
internal ushort Index;
11+
internal ushort ID => (ushort)(Index + 1);
12+
13+
public readonly List<FLPatternNote> Notes;
14+
public FLColor3 Color;
15+
public string? Name;
16+
17+
internal FLPattern()
18+
{
19+
Notes = new List<FLPatternNote>();
20+
Color = DefaultColor;
21+
}
22+
23+
internal void WritePatternNotes(EndianBinaryWriter w)
24+
{
25+
FLProjectWriter.Write16BitEvent(w, FLEvent.NewPattern, ID);
26+
27+
// Must be in order of AbsoluteTick
28+
Notes.Sort((n1, n2) => n1.AbsoluteTick.CompareTo(n2.AbsoluteTick));
29+
30+
w.WriteEnum(FLEvent.PatternNotes);
31+
FLProjectWriter.WriteArrayEventLength(w, (uint)Notes.Count * FLPatternNote.LEN);
32+
foreach (FLPatternNote note in Notes)
33+
{
34+
note.Write(w);
35+
}
36+
}
37+
internal void WriteColorAndNameIfNecessary(EndianBinaryWriter w)
38+
{
39+
// If you supply a name, you must supply a color
40+
// But a color can be here with no name
41+
if (Name is null && Color.Equals(DefaultColor))
42+
{
43+
return;
44+
}
45+
46+
FLProjectWriter.Write16BitEvent(w, FLEvent.NewPattern, ID);
47+
48+
if (Name is not null)
49+
{
50+
FLProjectWriter.WriteUTF16EventWithLength(w, FLEvent.PatternName, Name + '\0');
51+
}
52+
FLProjectWriter.Write32BitEvent(w, FLEvent.PatternColor, Color.GetFLValue());
53+
54+
// Dunno what these are, but they are always these 3 values no matter what I touch in the color picker.
55+
// Patterns don't have icons, and the preset name/colors don't affect it, so idk
56+
FLProjectWriter.Write32BitEvent(w, FLEvent.Unk_157, uint.MaxValue);
57+
FLProjectWriter.Write32BitEvent(w, FLEvent.Unk_158, uint.MaxValue);
58+
FLProjectWriter.Write32BitEvent(w, FLEvent.Unk_164, 0);
59+
}
60+
}

‎KFLP/FLPatternNote.cs

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
using Kermalis.EndianBinaryIO;
2+
3+
namespace Kermalis.FLP;
4+
5+
public struct FLPatternNote
6+
{
7+
internal const int LEN = 24;
8+
9+
public uint AbsoluteTick;
10+
public bool Slide;
11+
public FLChannel Channel;
12+
/// <summary>Infinite => 0</summary>
13+
public uint DurationTicks = 48;
14+
public byte Key;
15+
/// <summary>-1200 => 000, 0 => 120, +1200 => 240</summary>
16+
public byte Pitch = 120;
17+
/// <summary>0% => 0x00, 50% => 0x40, 100% => 0x80</summary>
18+
public byte Release = 0x40;
19+
/// <summary>0 through F</summary>
20+
public byte Color;
21+
public bool Portamento;
22+
/// <summary>100% left => 0x00, center => 0x40, 100% right => 0x80</summary>
23+
public byte Panpot = 0x40;
24+
/// <summary>0% => 0x00, "80%" => 0x64, 100% => 0x80</summary>
25+
public byte Velocity = 0x64;
26+
/// <summary>-100 => 0x00, 0 => 0x80, +100 => 0xFF</summary>
27+
public byte ModX = 0x80;
28+
/// <summary>-100 => 0x00, 0 => 0x80, +100 => 0xFF</summary>
29+
public byte ModY = 0x80;
30+
31+
public FLPatternNote(FLChannel chan)
32+
{
33+
Channel = chan;
34+
}
35+
36+
public readonly void Write(EndianBinaryWriter w)
37+
{
38+
w.WriteUInt32(AbsoluteTick);
39+
40+
w.WriteByte((byte)(Slide ? 8 : 0));
41+
w.WriteByte(0x40);
42+
w.WriteUInt16(Channel.Index);
43+
44+
w.WriteUInt32(DurationTicks);
45+
w.WriteUInt32(Key);
46+
47+
w.WriteByte(Pitch);
48+
w.WriteByte(0);
49+
w.WriteByte(Release);
50+
// 0 through F are colors with no porta, 0x10 is color0 with porta, 0x1F is colorF with porta
51+
w.WriteByte((byte)(Color + (Portamento ? 0x10 : 0)));
52+
53+
w.WriteByte(Panpot);
54+
w.WriteByte(Velocity);
55+
w.WriteByte(ModX);
56+
w.WriteByte(ModY);
57+
}
58+
}

‎KFLP/FLPlaylistItem.cs

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
using Kermalis.EndianBinaryIO;
2+
using System;
3+
4+
namespace Kermalis.FLP;
5+
6+
public sealed class FLPlaylistItem
7+
{
8+
internal const int LEN = 32;
9+
10+
public uint AbsoluteTick;
11+
public FLPattern? Pattern;
12+
public FLAutomation? Automation;
13+
public uint DurationTicks;
14+
public FLPlaylistTrack PlaylistTrack;
15+
16+
public FLPlaylistItem(uint tick, FLPattern pattern, uint duration, FLPlaylistTrack track)
17+
{
18+
AbsoluteTick = tick;
19+
Pattern = pattern;
20+
DurationTicks = duration;
21+
PlaylistTrack = track;
22+
}
23+
public FLPlaylistItem(uint tick, FLAutomation a, uint duration, FLPlaylistTrack track)
24+
{
25+
AbsoluteTick = tick;
26+
Automation = a;
27+
DurationTicks = duration;
28+
PlaylistTrack = track;
29+
}
30+
31+
internal void Write(EndianBinaryWriter w)
32+
{
33+
w.WriteUInt32(AbsoluteTick);
34+
35+
w.WriteUInt16(0x5000);
36+
if (Automation is not null)
37+
{
38+
w.WriteUInt16(Automation.Index);
39+
}
40+
else if (Pattern is not null)
41+
{
42+
w.WriteUInt16((ushort)(0x5000 + Pattern.ID));
43+
}
44+
else
45+
{
46+
throw new InvalidOperationException("Automation and Pattern were null");
47+
}
48+
49+
w.WriteUInt32(DurationTicks);
50+
51+
w.WriteUInt16((ushort)(FLArrangement.NUM_PLAYLIST_TRACKS - PlaylistTrack.ID));
52+
w.WriteUInt16(0);
53+
54+
w.WriteByte(0x78); // 120
55+
w.WriteByte(0);
56+
w.WriteByte(0x40); // 64
57+
w.WriteByte(0); // Flags: 0x80 if selected, 0x00 if deselected, 0x20 if disabled and deselected, 0xA0 if disabled and selected
58+
59+
w.WriteByte(0x40); // 64
60+
w.WriteByte(0x64); // 100
61+
w.WriteUInt16(0x8080);
62+
63+
if (Automation is not null)
64+
{
65+
w.WriteSingle(-1f);
66+
w.WriteSingle(-1f);
67+
}
68+
else
69+
{
70+
// Both are uint.MaxValue if not manually set to the duration
71+
w.WriteUInt32(0);
72+
w.WriteUInt32(DurationTicks);
73+
}
74+
}
75+
}

‎KFLP/FLPlaylistMarker.cs

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
using Kermalis.EndianBinaryIO;
2+
3+
namespace Kermalis.FLP;
4+
5+
public sealed class FLPlaylistMarker
6+
{
7+
public uint AbsoluteTicks;
8+
/// <summary>Empty uses default name "Marker #1" for example</summary>
9+
public string Name;
10+
public (byte num, byte denom)? TimeSig;
11+
12+
public FLPlaylistMarker(uint ticks, string name, (byte, byte)? timeSig)
13+
{
14+
AbsoluteTicks = ticks;
15+
Name = name;
16+
TimeSig = timeSig;
17+
}
18+
19+
internal void Write(EndianBinaryWriter w)
20+
{
21+
uint add = TimeSig is null ? 0u : 0x08_000_000;
22+
FLProjectWriter.Write32BitEvent(w, FLEvent.NewTimeMarker, AbsoluteTicks + add);
23+
24+
if (TimeSig is not null)
25+
{
26+
(byte num, byte denom) = TimeSig.Value;
27+
FLProjectWriter.Write8BitEvent(w, FLEvent.TimeSigMarkerNumerator, num);
28+
FLProjectWriter.Write8BitEvent(w, FLEvent.TimeSigMarkerDenominator, denom);
29+
}
30+
FLProjectWriter.WriteUTF16EventWithLength(w, FLEvent.TimeMarkerName, Name + '\0');
31+
}
32+
}

‎KFLP/FLPlaylistTrack.cs

+104
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
using Kermalis.EndianBinaryIO;
2+
3+
namespace Kermalis.FLP;
4+
5+
public sealed class FLPlaylistTrack
6+
{
7+
public const float SIZE_MIN = 0f;
8+
public const float SIZE_DEFAULT = 1f;
9+
public const float SIZE_MAX = 25.9249992370605f;
10+
public static FLColor3 DefaultColor => new(72, 81, 86);
11+
12+
internal readonly ushort Index;
13+
internal ushort ID => (ushort)(Index + 1);
14+
15+
public float Size;
16+
public bool GroupWithAbove;
17+
/// <summary>Only works if this track is the parent of the group</summary>
18+
public bool IsGroupCollapsed;
19+
public string? Name;
20+
public FLColor3 Color;
21+
public uint Icon;
22+
23+
internal FLPlaylistTrack(ushort index)
24+
{
25+
Index = index;
26+
Color = DefaultColor;
27+
Size = SIZE_DEFAULT;
28+
}
29+
30+
internal void Write(EndianBinaryWriter w, FLVersionCompat verCom)
31+
{
32+
WriteNewPlaylistTrack(w);
33+
if (verCom == FLVersionCompat.V21_0_3__B3517)
34+
{
35+
byte val = (byte)(Color.Equals(DefaultColor) ? 0 : 1);
36+
FLProjectWriter.Write8BitEvent(w, FLEvent.PlaylistTrackIgnoresTheme, val);
37+
}
38+
if (Name is not null)
39+
{
40+
FLProjectWriter.WriteUTF16EventWithLength(w, FLEvent.PlaylistTrackName, Name + '\0');
41+
}
42+
}
43+
private void WriteNewPlaylistTrack(EndianBinaryWriter w)
44+
{
45+
w.WriteEnum(FLEvent.NewPlaylistTrack);
46+
FLProjectWriter.WriteArrayEventLength(w, 66);
47+
48+
w.WriteUInt32(ID);
49+
50+
w.WriteByte(Color.R);
51+
w.WriteByte(Color.G);
52+
w.WriteByte(Color.B);
53+
w.WriteByte(0);
54+
55+
w.WriteUInt32(Icon);
56+
57+
w.WriteByte(1);
58+
59+
w.WriteSingle(Size);
60+
61+
// The default height in pixels is 56
62+
// If I "Lock to this size", this becomes 0x38 (56) instead of -16 or -1
63+
// If I manually resize it, this becomes -56 and Size (above) changes
64+
// Even if I reset the size to 100%, this stays -56 instead of going back to the weird value
65+
w.WriteInt32(Index <= 0x20 ? -16 : -1); // TODO: Why?
66+
67+
w.WriteByte(0);
68+
w.WriteByte(0); // Performance Motion
69+
w.WriteInt16(0);
70+
71+
w.WriteByte(0);
72+
w.WriteByte(0); // Performance Press
73+
w.WriteInt16(0);
74+
75+
w.WriteByte(0);
76+
w.WriteByte(5); // Performance Trigger Sync (4 beats)
77+
w.WriteInt16(0);
78+
79+
w.WriteByte(0);
80+
w.WriteBoolean(false); // Performance Queue
81+
w.WriteInt16(0);
82+
83+
w.WriteByte(0);
84+
w.WriteBoolean(true); // Performance Tolerant
85+
w.WriteInt16(0);
86+
87+
w.WriteByte(0);
88+
w.WriteByte(0); // Performance Position Sync
89+
w.WriteInt16(0);
90+
91+
w.WriteByte(0);
92+
w.WriteBoolean(GroupWithAbove);
93+
w.WriteInt16(0);
94+
95+
w.WriteInt32(0); // Was 1 in "track mode - audio track" and 3 in "track mode - instrument track"
96+
97+
w.WriteInt32(-1); // In audio track mode, it was the insert
98+
w.WriteInt32(-1); // In instrument track mode, it was the channelID
99+
100+
w.WriteBoolean(!IsGroupCollapsed);
101+
102+
w.WriteInt32(0); // Track Mode Instrument Track Options
103+
}
104+
}

‎KFLP/FLPluginParams.cs

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
using Kermalis.EndianBinaryIO;
2+
using System.Text;
3+
4+
namespace Kermalis.FLP;
5+
6+
internal struct FLPluginParams
7+
{
8+
public static void WriteMIDIOut(EndianBinaryWriter w, byte midiChannel, byte midiBank, byte program)
9+
{
10+
w.WriteEnum(FLEvent.PluginParams);
11+
FLProjectWriter.WriteArrayEventLength(w, 383);
12+
13+
w.WriteUInt32(6);
14+
w.WriteUInt32(midiChannel);
15+
w.WriteInt32(-1);
16+
w.WriteInt32(-1);
17+
w.WriteInt32(-1);
18+
w.WriteInt32(0);
19+
w.WriteInt32(midiBank);
20+
21+
w.WriteUInt16(1);
22+
w.WriteUInt16((byte)(program + 1));
23+
24+
w.WriteZeroes(290);
25+
26+
w.WriteSByte(-1);
27+
for (int i = 1; i <= 8; i++)
28+
{
29+
string s = "Page " + i;
30+
w.WriteByte((byte)s.Length);
31+
w.WriteChars(s);
32+
}
33+
34+
w.WriteUInt32(0);
35+
}
36+
37+
public static void WriteFruityLSD(EndianBinaryWriter w, byte bankID, string dlsPath)
38+
{
39+
byte[] pathBytes = Encoding.UTF8.GetBytes(dlsPath);
40+
byte dlsPathLen = (byte)pathBytes.Length;
41+
42+
w.WriteEnum(FLEvent.PluginParams);
43+
FLProjectWriter.WriteArrayEventLength(w, (uint)(97 + dlsPathLen));
44+
45+
w.WriteUInt32(0);
46+
w.WriteUInt32(0x80); // 128
47+
w.WriteUInt32(0);
48+
w.WriteUInt32(0);
49+
w.WriteUInt32(0);
50+
w.WriteUInt32(0);
51+
w.WriteUInt32(0);
52+
w.WriteUInt32(bankID);
53+
54+
w.WriteByte(dlsPathLen); // It's possible this is a varLen length, but I didn't check
55+
w.WriteBytes(pathBytes);
56+
57+
w.WriteZeroes(7);
58+
59+
w.WriteUInt32(0);
60+
w.WriteUInt32(0);
61+
w.WriteUInt32(0);
62+
w.WriteUInt32(0);
63+
w.WriteUInt32(0);
64+
w.WriteUInt32(0);
65+
w.WriteUInt32(0);
66+
w.WriteUInt32(0);
67+
w.WriteUInt32(0x80); // 128
68+
w.WriteUInt32(0);
69+
w.WriteUInt32(0);
70+
w.WriteUInt32(0);
71+
w.WriteUInt32(0);
72+
w.WriteUInt32(0);
73+
w.WriteByte(0);
74+
}
75+
}

‎KFLP/FLProjectReader.cs

+362
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,362 @@
1+
using Kermalis.EndianBinaryIO;
2+
using System;
3+
using System.IO;
4+
using System.Linq;
5+
using System.Text;
6+
7+
namespace Kermalis.FLP;
8+
9+
public sealed class FLProjectReader
10+
{
11+
public readonly ushort PPQN;
12+
public readonly StringBuilder Log;
13+
14+
public FLProjectReader(Stream s)
15+
{
16+
var r = new EndianBinaryReader(s, ascii: true);
17+
18+
ReadHeaderChunk(r, out PPQN);
19+
20+
Log = new StringBuilder();
21+
22+
ReadDataChunk(r);
23+
}
24+
private static void ReadHeaderChunk(EndianBinaryReader r, out ushort ppqn)
25+
{
26+
Span<char> chars = stackalloc char[4];
27+
r.ReadChars(chars);
28+
29+
if (!chars.SequenceEqual("FLhd"))
30+
{
31+
throw new InvalidDataException();
32+
}
33+
34+
uint headerLen = r.ReadUInt32();
35+
if (headerLen != 6)
36+
{
37+
throw new InvalidDataException();
38+
}
39+
40+
ushort format = r.ReadUInt16();
41+
if (format != 0)
42+
{
43+
throw new InvalidDataException();
44+
}
45+
46+
ushort numChannels = r.ReadUInt16();
47+
if (numChannels is < 1 or > 1000)
48+
{
49+
throw new InvalidDataException();
50+
}
51+
52+
ppqn = r.ReadUInt16();
53+
}
54+
private void ReadDataChunk(EndianBinaryReader r)
55+
{
56+
Span<char> chars = stackalloc char[4];
57+
r.ReadChars(chars);
58+
59+
if (!chars.SequenceEqual("FLdt"))
60+
{
61+
throw new InvalidDataException();
62+
}
63+
64+
uint dataLen = r.ReadUInt32();
65+
if (dataLen >= 0x10_000_000 || dataLen != r.Stream.Length - r.Stream.Position)
66+
{
67+
throw new InvalidDataException();
68+
}
69+
70+
while (r.Stream.Position < r.Stream.Length)
71+
{
72+
LogPos(r.Stream.Position);
73+
74+
FLEvent ev = r.ReadEnum<FLEvent>();
75+
uint data = ReadDataForEvent(r, ev, out byte[]? bytes);
76+
77+
if (ev < FLEvent.NewChannel)
78+
{
79+
HandleEvent_8Bit(ev, data);
80+
}
81+
else if (ev is >= FLEvent.NewChannel and < FLEvent.PluginColor)
82+
{
83+
HandleEvent_16Bit(ev, data);
84+
}
85+
else if (ev is >= FLEvent.PluginColor and < FLEvent.ChannelName)
86+
{
87+
HandleEvent_32Bit(ev, data);
88+
}
89+
else // >= FLEvent.ChanName
90+
{
91+
HandleEvent_Array(ev, bytes!);
92+
}
93+
}
94+
}
95+
private static uint ReadDataForEvent(EndianBinaryReader r, FLEvent ev, out byte[]? bytes)
96+
{
97+
bytes = null;
98+
99+
uint data = r.ReadByte();
100+
101+
if (ev is >= FLEvent.NewChannel and < FLEvent.ChannelName)
102+
{
103+
data |= (uint)r.ReadByte() << 8;
104+
}
105+
if (ev is >= FLEvent.PluginColor and < FLEvent.ChannelName)
106+
{
107+
data |= (uint)r.ReadByte() << 16;
108+
data |= (uint)r.ReadByte() << 24;
109+
}
110+
if (ev >= FLEvent.ChannelName)
111+
{
112+
// Only have the first byte in data currently
113+
bytes = new byte[ReadArrayLen(r, data)];
114+
r.ReadBytes(bytes);
115+
}
116+
117+
return data;
118+
}
119+
private void HandleEvent_8Bit(FLEvent ev, uint data)
120+
{
121+
switch (ev)
122+
{
123+
case FLEvent.ChannelType:
124+
{
125+
LogLine(string.Format("Byte: {0} = {1} ({2})", ev, data, (FLChannelType)data));
126+
break;
127+
}
128+
default:
129+
{
130+
CheckEventExists(ev);
131+
132+
LogLine(string.Format("Byte: {0} = {1}", ev, data));
133+
break;
134+
}
135+
}
136+
}
137+
private void HandleEvent_16Bit(FLEvent ev, uint data)
138+
{
139+
switch (ev)
140+
{
141+
case FLEvent.Fade_Stereo:
142+
{
143+
LogLine(string.Format("Word: {0} = 0x{1:X} ({2})", ev, data, (FLFadeStereo)data));
144+
break;
145+
}
146+
case FLEvent.SwingMix:
147+
case FLEvent.FX:
148+
case FLEvent.FX3:
149+
case FLEvent.StDel:
150+
case FLEvent.CutOff:
151+
{
152+
LogLine(string.Format("Word: {0} = 0x{1:X}", ev, data));
153+
break;
154+
}
155+
default:
156+
{
157+
CheckEventExists(ev);
158+
159+
LogLine(string.Format("Word: {0} = {1}", ev, data));
160+
break;
161+
}
162+
}
163+
}
164+
private void HandleEvent_32Bit(FLEvent ev, uint data)
165+
{
166+
switch (ev)
167+
{
168+
case FLEvent.PluginColor:
169+
case FLEvent.PatternColor:
170+
case FLEvent.InsertColor:
171+
{
172+
LogLine(string.Format("DWord: {0} = 0x{1:X6} ({2})", ev, data, new FLColor3(data)));
173+
break;
174+
}
175+
case FLEvent.DelayReso:
176+
case FLEvent.Reverb:
177+
case FLEvent.FXSine:
178+
case FLEvent.CutCutBy:
179+
case FLEvent.ChannelLayerFlags:
180+
case FLEvent.ChannelSampleFlags:
181+
case FLEvent.InsertInChanNum:
182+
case FLEvent.InsertOutChanNum:
183+
case FLEvent.Unk_157:
184+
case FLEvent.Unk_158:
185+
case FLEvent.NewTimeMarker:
186+
{
187+
LogLine(string.Format("DWord: {0} = 0x{1:X}", ev, data));
188+
break;
189+
}
190+
default:
191+
{
192+
CheckEventExists(ev);
193+
194+
LogLine(string.Format("DWord: {0} = {1}", ev, data));
195+
break;
196+
}
197+
}
198+
}
199+
private void HandleEvent_Array(FLEvent ev, byte[] bytes)
200+
{
201+
string type;
202+
string str;
203+
204+
if (ev == FLEvent.AutomationData)
205+
{
206+
type = "Bytes";
207+
str = FLAutomation.ReadData(bytes);
208+
}
209+
else if (ev == FLEvent.MixerParams)
210+
{
211+
type = "Bytes";
212+
str = FLMixerParams.ReadData(bytes);
213+
}
214+
else if (ev == FLEvent.ProjectTime)
215+
{
216+
type = "Bytes";
217+
str = FLProjectTime.ReadData(bytes);
218+
}
219+
else
220+
{
221+
CheckEventExists(ev);
222+
223+
if (IsUTF8(ev))
224+
{
225+
type = "UTF8";
226+
str = DecodeString(Encoding.UTF8, bytes);
227+
}
228+
else if (IsUTF16(ev))
229+
{
230+
type = "UTF16";
231+
str = DecodeString(Encoding.Unicode, bytes);
232+
}
233+
else
234+
{
235+
type = "Bytes";
236+
str = BytesString(bytes);
237+
}
238+
}
239+
LogLine(string.Format("{0}: {1} - {2} = {3}",
240+
type, ev, bytes.Length, str));
241+
}
242+
243+
private void LogPos(long pos)
244+
{
245+
Log.Append($"@ 0x{pos:X}\t");
246+
if (pos < 0x1000)
247+
{
248+
Log.Append('\t');
249+
}
250+
}
251+
private void LogLine(string msg)
252+
{
253+
Log.AppendLine(msg);
254+
}
255+
256+
private static string DecodeString(Encoding e, byte[] bytes)
257+
{
258+
string str = e.GetString(bytes)
259+
.Replace("\0", "\\0")
260+
.Replace("\r", "\\r")
261+
.Replace("\n", "\\n");
262+
return '\"' + str + '\"';
263+
}
264+
private static string BytesString(byte[] bytes)
265+
{
266+
if (bytes.Length == 0)
267+
{
268+
return "[]";
269+
}
270+
return "[0x" + string.Join(", 0x", bytes.Select(b => b.ToString("X2"))) + ']';
271+
}
272+
273+
// https://github.com/jdstmporter/FLPFiles/tree/main/src/FLP/messagetypes
274+
private static bool IsObsolete(FLEvent ev)
275+
{
276+
switch (ev)
277+
{
278+
// Byte
279+
case FLEvent.ChannelVolume:
280+
case FLEvent.ChannelPanpot:
281+
case FLEvent.MainVolume: // Now stored in _initCtrlRecChan
282+
case FLEvent.FitToSteps:
283+
case FLEvent.Pitchable:
284+
case FLEvent.DelayFlags:
285+
case FLEvent.NStepsShown:
286+
287+
// Word
288+
case FLEvent.Tempo: // FineTempo is used now
289+
case FLEvent.RandChan:
290+
case FLEvent.MixChan:
291+
case FLEvent.OldSongLoopPos:
292+
case FLEvent.TempoFine: // FineTempo is used now
293+
294+
// DWord
295+
case FLEvent.PlaylistItem: // PlaylistItems now
296+
case FLEvent.MainResoCutOff:
297+
case FLEvent.SSNote:
298+
case FLEvent.PatternAutoMode:
299+
300+
// Text
301+
case FLEvent.ChannelName: // PluginName is used now
302+
case FLEvent.DelayLine:
303+
case FLEvent.OldFilterParams:
304+
return true;
305+
}
306+
return false;
307+
}
308+
private static bool IsUTF8(FLEvent ev)
309+
{
310+
switch (ev)
311+
{
312+
case FLEvent.Version:
313+
return true;
314+
}
315+
return false;
316+
}
317+
private static bool IsUTF16(FLEvent ev)
318+
{
319+
switch (ev)
320+
{
321+
case FLEvent.ProjectTitle:
322+
case FLEvent.ProjectComment:
323+
case FLEvent.ProjectURL:
324+
case FLEvent.RegistrationID:
325+
case FLEvent.DefPluginName:
326+
case FLEvent.ProjectDataPath:
327+
case FLEvent.PluginName:
328+
case FLEvent.InsertName:
329+
case FLEvent.TimeMarkerName:
330+
case FLEvent.ProjectGenre:
331+
case FLEvent.ProjectAuthor:
332+
case FLEvent.RemoteCtrlFormula:
333+
case FLEvent.ChanFilterName:
334+
case FLEvent.PlaylistTrackName:
335+
case FLEvent.PlaylistArrangementName:
336+
case FLEvent.PatternName:
337+
return true;
338+
}
339+
return false;
340+
}
341+
private void CheckEventExists(FLEvent ev)
342+
{
343+
if (!Enum.IsDefined(ev))
344+
{
345+
LogLine("!!!!!!!!!!!!!!! UNDEFINED EVENT " + ev + " !!!!!!!!!!!!!!!");
346+
}
347+
}
348+
349+
private static uint ReadArrayLen(EndianBinaryReader r, uint curByte)
350+
{
351+
// TODO: How many bytes can this len use?
352+
uint len = curByte & 0x7F;
353+
byte shift = 0;
354+
while ((curByte & 0x80) != 0)
355+
{
356+
shift += 7;
357+
curByte = r.ReadByte();
358+
len |= (curByte & 0x7F) << shift;
359+
}
360+
return len;
361+
}
362+
}

‎KFLP/FLProjectTime.cs

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using Kermalis.EndianBinaryIO;
2+
using System;
3+
4+
namespace Kermalis.FLP;
5+
6+
internal struct FLProjectTime
7+
{
8+
private static DateTime BaseDate => new(1899, 12, 30);
9+
10+
public static string ReadData(byte[] bytes)
11+
{
12+
double startOffset = EndianBinaryPrimitives.ReadDouble(bytes, Endianness.LittleEndian);
13+
double daysWorked = EndianBinaryPrimitives.ReadDouble(bytes.AsSpan(8), Endianness.LittleEndian);
14+
return string.Format("{{ Created: {0}, TimeSpent: {1} }}", BaseDate.AddDays(startOffset), TimeSpan.FromDays(daysWorked));
15+
}
16+
17+
public static void Write(EndianBinaryWriter w, DateTime creationDateTime, TimeSpan timeSpent)
18+
{
19+
w.WriteEnum(FLEvent.ProjectTime);
20+
FLProjectWriter.WriteArrayEventLength(w, 16);
21+
22+
w.WriteDouble((creationDateTime - BaseDate).TotalDays);
23+
w.WriteDouble(timeSpent.TotalDays);
24+
}
25+
}

‎KFLP/FLProjectWriter.cs

+395
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,395 @@
1+
using Kermalis.EndianBinaryIO;
2+
using System;
3+
using System.Collections.Generic;
4+
using System.IO;
5+
using System.Text;
6+
7+
namespace Kermalis.FLP;
8+
9+
public sealed class FLProjectWriter
10+
{
11+
private static ReadOnlySpan<byte> MIDIInfo0 => new byte[20]
12+
{
13+
0x01, 0x00, 0x00, 0x00,
14+
0x00, 0x00, 0x00, 0x00,
15+
0x01,
16+
0x90, // 144
17+
0xFF, 0x0F,
18+
0x04, 0x00, 0x00, 0x00,
19+
0xD5, 0x01, // 469
20+
0x00, 0x00
21+
};
22+
private static ReadOnlySpan<byte> MIDIInfo1 => new byte[20]
23+
{
24+
0xFD, 0x00, 0x00, 0x00,
25+
0x00, 0x00, 0x00, 0x00,
26+
0x80, // 128
27+
0x90, // 144
28+
0xFF, 0x0F,
29+
0x04, 0x00, 0x00, 0x00,
30+
0xD5, 0x01, // 469
31+
0x00, 0x00
32+
};
33+
private static ReadOnlySpan<byte> MIDIInfo2 => new byte[20]
34+
{
35+
0xFF, 0x00, 0x00, 0x00,
36+
0x00, 0x00, 0x00, 0x00,
37+
0x04, // 4
38+
0x00,
39+
0xFF, 0x0F,
40+
0x04, 0x00, 0x00, 0x00,
41+
0x00, 0xFE,
42+
0xFF, 0xFF
43+
};
44+
45+
public readonly FLInsert[] Inserts;
46+
public readonly List<FLChannelFilter> ChannelFilters;
47+
public readonly List<FLChannel> Channels;
48+
public readonly List<FLAutomation> Automations;
49+
public readonly List<FLPattern> Patterns;
50+
public readonly List<FLArrangement> Arrangements;
51+
public FLVersionCompat VersionCompat;
52+
public ushort PPQN;
53+
public decimal CurrentTempo;
54+
public byte TimeSigNumerator;
55+
public byte TimeSigDenominator;
56+
public FLChannelFilter? SelectedChannelFilter;
57+
public FLArrangement? SelectedArrangement;
58+
59+
public FLProjectWriter()
60+
{
61+
// FL Default
62+
PPQN = 96;
63+
CurrentTempo = 140;
64+
TimeSigNumerator = 4;
65+
TimeSigDenominator = 4;
66+
67+
ChannelFilters = new List<FLChannelFilter>();
68+
Channels = new List<FLChannel>();
69+
Automations = new List<FLAutomation>();
70+
Patterns = new List<FLPattern>();
71+
Arrangements = new List<FLArrangement>(1)
72+
{
73+
new FLArrangement("Arrangement"),
74+
};
75+
76+
Inserts = new FLInsert[127];
77+
for (byte i = 0; i < 127; i++)
78+
{
79+
Inserts[i] = new FLInsert(i);
80+
}
81+
}
82+
83+
public FLChannelFilter CreateUnsortedFilter()
84+
{
85+
return CreateChannelFilter("Unsorted");
86+
}
87+
public FLChannelFilter CreateAutomationFilter()
88+
{
89+
return CreateChannelFilter("Automation");
90+
}
91+
public FLChannelFilter CreateChannelFilter(string name)
92+
{
93+
var f = new FLChannelFilter(name);
94+
ChannelFilters.Add(f);
95+
return f;
96+
}
97+
98+
public FLChannel CreateChannel(string name, byte midiChan, byte midiBank, byte midiProgram, FLChannelFilter filter)
99+
{
100+
var c = new FLChannel(name, midiChan, midiBank, midiProgram, filter);
101+
Channels.Add(c);
102+
return c;
103+
}
104+
public FLAutomation CreateTempoAutomation(string name, FLChannelFilter filter)
105+
{
106+
var a = new FLAutomation(name, FLAutomation.MyType.Tempo, null, filter);
107+
Automations.Add(a);
108+
return a;
109+
}
110+
public FLAutomation CreateAutomation(string name, FLAutomation.MyType type, List<FLChannel> targets, FLChannelFilter filter)
111+
{
112+
var a = new FLAutomation(name, type, targets, filter);
113+
Automations.Add(a);
114+
return a;
115+
}
116+
public FLAutomation CreateAutomation(string name, FLAutomation.MyType type, FLChannel target, FLChannelFilter filter)
117+
{
118+
var a = new FLAutomation(name, type, new List<FLChannel>(1) { target }, filter);
119+
Automations.Add(a);
120+
return a;
121+
}
122+
123+
public FLPattern CreatePattern()
124+
{
125+
var p = new FLPattern();
126+
Patterns.Add(p);
127+
return p;
128+
}
129+
130+
public void Write(Stream s)
131+
{
132+
var w = new EndianBinaryWriter(s, ascii: true);
133+
134+
FirstPassAssignIDs();
135+
136+
WriteHeaderChunk(w);
137+
WriteDataChunk(w);
138+
}
139+
private void FirstPassAssignIDs()
140+
{
141+
// Channel Filters must be alphabetical. Even if I don't sort them, they will be opened in the wrong order
142+
ChannelFilters.Sort((c1, c2) => c1.Name.CompareTo(c2.Name));
143+
for (ushort i = 0; i < ChannelFilters.Count; i++)
144+
{
145+
ChannelFilters[i].Index = i;
146+
}
147+
ushort chanIndex = 0;
148+
foreach (FLChannel c in Channels)
149+
{
150+
c.Index = chanIndex++;
151+
}
152+
foreach (FLAutomation a in Automations)
153+
{
154+
a.Index = chanIndex++;
155+
}
156+
for (ushort i = 0; i < Patterns.Count; i++)
157+
{
158+
Patterns[i].Index = i;
159+
}
160+
for (ushort i = 0; i < Arrangements.Count; i++)
161+
{
162+
Arrangements[i].Index = i;
163+
}
164+
}
165+
private void WriteHeaderChunk(EndianBinaryWriter w)
166+
{
167+
w.WriteChars("FLhd");
168+
w.WriteUInt32(6); // Length
169+
w.WriteUInt16(0); // Format
170+
w.WriteUInt16((ushort)(Channels.Count + Automations.Count));
171+
w.WriteUInt16(PPQN);
172+
}
173+
private void WriteDataChunk(EndianBinaryWriter w)
174+
{
175+
w.WriteChars("FLdt");
176+
177+
long dataLenOffset = w.Stream.Position;
178+
w.WriteUInt32(0); // Write length later
179+
180+
WriteProjectInfo(w);
181+
WriteChannels(w);
182+
foreach (FLArrangement a in Arrangements)
183+
{
184+
a.Write(w, VersionCompat);
185+
}
186+
WriteMoreStuffIDK(w);
187+
WriteMixer(w);
188+
189+
// Write chunk length
190+
uint length = (uint)(w.Stream.Length - (dataLenOffset + 4));
191+
w.Stream.Position = dataLenOffset;
192+
w.WriteUInt32(length);
193+
}
194+
195+
internal static void Write8BitEvent(EndianBinaryWriter w, FLEvent ev, byte data)
196+
{
197+
w.WriteEnum(ev);
198+
w.WriteByte(data);
199+
}
200+
internal static void Write16BitEvent(EndianBinaryWriter w, FLEvent ev, ushort data)
201+
{
202+
w.WriteEnum(ev);
203+
w.WriteUInt16(data);
204+
}
205+
internal static void Write32BitEvent(EndianBinaryWriter w, FLEvent ev, uint data)
206+
{
207+
w.WriteEnum(ev);
208+
w.WriteUInt32(data);
209+
}
210+
internal static void WriteArrayEventLength(EndianBinaryWriter w, uint length)
211+
{
212+
// TODO: How many bytes can this len use?
213+
while (true)
214+
{
215+
if (length <= 0x7F)
216+
{
217+
w.WriteByte((byte)length);
218+
return;
219+
}
220+
221+
w.WriteByte((byte)((length & 0x7Fu) | 0x80u));
222+
length >>= 7;
223+
}
224+
}
225+
internal static void WriteUTF8EventWithLength(EndianBinaryWriter w, FLEvent ev, string str)
226+
{
227+
WriteArrayEventWithLength(w, ev, Encoding.UTF8.GetBytes(str));
228+
}
229+
internal static void WriteUTF16EventWithLength(EndianBinaryWriter w, FLEvent ev, string str)
230+
{
231+
WriteArrayEventWithLength(w, ev, Encoding.Unicode.GetBytes(str));
232+
}
233+
internal static void WriteArrayEventWithLength(EndianBinaryWriter w, FLEvent ev, ReadOnlySpan<byte> bytes)
234+
{
235+
w.WriteEnum(ev);
236+
WriteArrayEventLength(w, (uint)bytes.Length);
237+
w.WriteBytes(bytes);
238+
}
239+
240+
private void WriteProjectInfo(EndianBinaryWriter w)
241+
{
242+
WriteVersion(w);
243+
WriteRegistration(w);
244+
245+
Write32BitEvent(w, FLEvent.FineTempo, (uint)(CurrentTempo * 1_000));
246+
Write16BitEvent(w, FLEvent.SelectedPatternNum, 1);
247+
Write8BitEvent(w, FLEvent.IsSongMode, 1);
248+
Write8BitEvent(w, FLEvent.Shuffle, 0);
249+
Write16BitEvent(w, FLEvent.MasterPitch, 0);
250+
Write8BitEvent(w, FLEvent.ProjectTimeSigNumerator, TimeSigNumerator);
251+
Write8BitEvent(w, FLEvent.ProjectTimeSigDenominator, TimeSigDenominator);
252+
Write8BitEvent(w, FLEvent.ProjectShouldUseTimeSignatures, 1);
253+
Write8BitEvent(w, FLEvent.PanningLaw, 0); // Circular
254+
if (VersionCompat == FLVersionCompat.V21_0_3__B3517)
255+
{
256+
Write8BitEvent(w, FLEvent.PlaylistShouldUseAutoCrossfades, 0);
257+
}
258+
Write8BitEvent(w, FLEvent.ShouldPlayTruncatedClipNotes, 1);
259+
Write8BitEvent(w, FLEvent.ShouldShowInfoOnOpen, 0);
260+
WriteUTF16EventWithLength(w, FLEvent.ProjectTitle, "\0");
261+
WriteUTF16EventWithLength(w, FLEvent.ProjectGenre, "\0");
262+
WriteUTF16EventWithLength(w, FLEvent.ProjectAuthor, "\0");
263+
WriteUTF16EventWithLength(w, FLEvent.ProjectDataPath, "\0");
264+
WriteUTF16EventWithLength(w, FLEvent.ProjectComment, "\0");
265+
// ProjectURL would go here
266+
FLProjectTime.Write(w, DateTime.Now, TimeSpan.Zero);
267+
268+
foreach (FLChannelFilter f in ChannelFilters)
269+
{
270+
f.Write(w);
271+
}
272+
Write32BitEvent(w, FLEvent.CurFilterNum, SelectedChannelFilter is null ? 0 : (uint)SelectedChannelFilter.Index);
273+
274+
WriteArrayEventWithLength(w, FLEvent.CtrlRecChan, Array.Empty<byte>());
275+
// TODO: Why did this CtrlRecChan only show up sometimes? 0x1900 = 6_400
276+
// Bytes: CtrlRecChan - 12 = [0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x19, 0x00, 0x00]
277+
foreach (FLPattern p in Patterns)
278+
{
279+
p.WritePatternNotes(w);
280+
}
281+
282+
// No idea what these mean, but there are 3 and they're always the same in all my projects.
283+
// Dunno if it's MIDI keyboard related or something, because I only have 1
284+
WriteArrayEventWithLength(w, FLEvent.MIDIInfo, MIDIInfo0);
285+
WriteArrayEventWithLength(w, FLEvent.MIDIInfo, MIDIInfo1);
286+
WriteArrayEventWithLength(w, FLEvent.MIDIInfo, MIDIInfo2);
287+
}
288+
private void WriteVersion(EndianBinaryWriter w)
289+
{
290+
string v;
291+
uint b;
292+
293+
switch (VersionCompat)
294+
{
295+
case FLVersionCompat.V20_9_2__B2963:
296+
{
297+
v = "20.9.2.2963";
298+
b = 2963;
299+
break;
300+
}
301+
case FLVersionCompat.V21_0_3__B3517:
302+
{
303+
v = "21.0.3.3517";
304+
b = 3517;
305+
break;
306+
}
307+
default: throw new InvalidOperationException("Invalid FL Version compatibility: " + VersionCompat);
308+
}
309+
310+
WriteUTF8EventWithLength(w, FLEvent.Version, v + '\0');
311+
Write32BitEvent(w, FLEvent.VersionBuildNumber, b);
312+
}
313+
private static void WriteRegistration(EndianBinaryWriter w)
314+
{
315+
// When you save a project in FL Studio, the entire file becomes obfuscated if IsRegistered is 0 (trial mode).
316+
// Specifically, every 8bit/16bit/32bit event becomes obfuscated. The other array data is left alone (from the small glimpse I had), and it makes sense to leave it.
317+
// For example, the Unk_37 byte here became 76 instead of 1 in FL21. I didn't try different projects or FL versions, but if I had to guess, it is probably randomized and seeds the obfuscation.
318+
// Every other event also became obfuscated in some way that I couldn't quickly decipher with Windows calculator since it seems to mix the current position and eventID with the seed.
319+
// Examples:
320+
// ================
321+
// Byte: IsSongMode = 1 | IsSongMode = 62 (0x3E)
322+
// Byte: Shuffle = 0 | Shuffle = 61 (0x3D)
323+
// Byte: ProjectShouldUseTimeSignatures = 1 | ProjectShouldUseTimeSignatures = 53 (0x35)
324+
// Byte: PanningLaw = 0 | PanningLaw = 50 (0x32)
325+
// ================
326+
// Word: MasterPitch = 0 | MasterPitch = 15931 (0x3E3B)
327+
// Word: NewPattern = 1 | NewPattern = 8220 (0x201C)
328+
// Word: NewPattern = 2 | NewPattern = 6677 (0x1A15)
329+
// ================
330+
// DWord: FineTempo = 185000 | FineTempo = 1347393775 (0x504F98EF)
331+
// DWord: CurFilterNum = 0 | CurFilterNum = 757737252 (0x2D2A2724)
332+
333+
// I can only imagine that trying to decipher this is trouble waiting to happen, so I won't try to.
334+
// They clearly want to obfuscate trial projects in a proprietary way, in order to prevent people from using FL in a trial and then using the project files with other software.
335+
// Reverse-engineering is protected by law in the USA (where I live), but Image-Line can make it against their TOS to try to decipher trial mode projects. They probably have, but I didn't check.
336+
// If you ever manage to reverse-engineer their method, they will 100% just change it to a new one, then you can't support new versions lol
337+
338+
// They probably don't obfuscate the registered projects since it'd defeat the purpose of buying FL if you were trying to do something outside of it (like this).
339+
// If paying wouldn't make the project easier to parse, and you could defeat the obfuscation algorithm, you'd remain on the trial version or pirate it which is against Image-Line's interests.
340+
341+
// Anyway, FL seems to be fine when reading projects that are in trial mode, even if they have no obfuscation. I assume this is due to the Unk_37 byte here still being 1 instead of 76.
342+
// So I am writing unregistered projects with no obfuscation. Thanks Image-Line for making this possible :)
343+
344+
// I put my registration ID there as a reference to what it might look like.
345+
// I assume this is nothing to worry about since that would be so easy to find if you open a project I share. I've also seen other parsing projects show theirs.
346+
347+
// Hopefully you enjoyed reading this part
348+
349+
Write8BitEvent(w, FLEvent.IsRegistered, 0); // 1 if registered
350+
Write8BitEvent(w, FLEvent.Unk_37, 1); // Obfuscation-related?
351+
WriteUTF16EventWithLength(w, FLEvent.RegistrationID, "\0"); // Mine is "d3@?4xufs49p1n?B>;?889\0" in FL20 and FL21
352+
}
353+
private void WriteChannels(EndianBinaryWriter w)
354+
{
355+
foreach (FLAutomation a in Automations)
356+
{
357+
a.WriteAutomationConnection(w);
358+
}
359+
360+
foreach (FLChannel c in Channels)
361+
{
362+
c.Write(w, VersionCompat);
363+
}
364+
// For some reason, pattern colors go between
365+
foreach (FLPattern p in Patterns)
366+
{
367+
p.WriteColorAndNameIfNecessary(w);
368+
}
369+
//
370+
foreach (FLAutomation a in Automations)
371+
{
372+
a.Write(w, VersionCompat, PPQN);
373+
}
374+
}
375+
private void WriteMoreStuffIDK(EndianBinaryWriter w)
376+
{
377+
Write16BitEvent(w, FLEvent.CurArrangementNum, SelectedArrangement is null ? (ushort)0 : SelectedArrangement.Index);
378+
Write8BitEvent(w, FLEvent.APDC, 1);
379+
Write8BitEvent(w, FLEvent.Unk_39, 1);
380+
Write8BitEvent(w, FLEvent.ShouldCutNotesFast, 0);
381+
Write8BitEvent(w, FLEvent.EEAutoMode, 0);
382+
Write8BitEvent(w, FLEvent.Unk_38, 1);
383+
}
384+
private void WriteMixer(EndianBinaryWriter w)
385+
{
386+
foreach (FLInsert i in Inserts)
387+
{
388+
i.Write(w, VersionCompat);
389+
}
390+
391+
FLMixerParams.Write(w);
392+
393+
Write32BitEvent(w, FLEvent.WindowH, 1286); // TODO: WindowH for what? Piano roll? Mixer?
394+
}
395+
}

‎KFLP/FLUtils.cs

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
namespace Kermalis.FLP;
2+
3+
public static class FLUtils
4+
{
5+
/// <summary>Maps a value in the range [a1, a2] to [b1, b2]. Divide by zero occurs if a1 and a2 are equal</summary>
6+
public static float LerpUnclamped(float a1, float a2, float b1, float b2, float value)
7+
{
8+
return b1 + ((value - a1) / (a2 - a1) * (b2 - b1));
9+
}
10+
/// <inheritdoc cref="LerpUnclamped(float, float, float, float, float)"/>
11+
public static double LerpUnclamped(double a1, double a2, double b1, double b2, double value)
12+
{
13+
return b1 + ((value - a1) / (a2 - a1) * (b2 - b1));
14+
}
15+
/// <inheritdoc cref="LerpUnclamped(float, float, float, float, float)"/>
16+
public static decimal LerpUnclamped(decimal a1, decimal a2, decimal b1, decimal b2, decimal value)
17+
{
18+
return b1 + ((value - a1) / (a2 - a1) * (b2 - b1));
19+
}
20+
}

‎KFLP/KFLP.csproj

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net7.0</TargetFramework>
5+
<OutputType>Library</OutputType>
6+
<LangVersion>latest</LangVersion>
7+
<RootNamespace>Kermalis.FLP</RootNamespace>
8+
<Nullable>enable</Nullable>
9+
10+
<Authors>Kermalis</Authors>
11+
<Copyright>Kermalis</Copyright>
12+
<Product>KFLP</Product>
13+
<Title>KFLP</Title>
14+
<PackageId>KFLP</PackageId>
15+
<AssemblyName>KFLP</AssemblyName>
16+
<Version>1.0.0</Version>
17+
<RepositoryUrl>https://github.com/Kermalis/KFLP</RepositoryUrl>
18+
<RepositoryType>git</RepositoryType>
19+
</PropertyGroup>
20+
21+
<ItemGroup>
22+
<PackageReference Include="EndianBinaryIO" Version="2.1.0" />
23+
</ItemGroup>
24+
25+
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
26+
<GenerateSerializationAssemblies>Auto</GenerateSerializationAssemblies>
27+
<DebugType>none</DebugType>
28+
<DebugSymbols>false</DebugSymbols>
29+
</PropertyGroup>
30+
31+
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
32+
<Optimize>false</Optimize>
33+
<DefineConstants>DEBUG;TRACE</DefineConstants>
34+
<DebugType>full</DebugType>
35+
<DebugSymbols>true</DebugSymbols>
36+
</PropertyGroup>
37+
38+
</Project>

‎LICENSE.md

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2023 Kermalis
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

0 commit comments

Comments
 (0)
Please sign in to comment.