ts-flp 1.0.4 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -674
- package/README.md +12 -1
- package/dist/index.cjs +174 -78
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +174 -78
- package/dist/index.js.map +1 -1
- package/dist/src/api/ProjectApi.d.ts +1 -1
- package/dist/src/api/ProjectApi.d.ts.map +1 -1
- package/dist/src/generated/events.generated.d.ts +1 -1
- package/dist/src/generated/events.generated.d.ts.map +1 -1
- package/dist/src/parser/FlpParser.d.ts +9 -3
- package/dist/src/parser/FlpParser.d.ts.map +1 -1
- package/package.json +8 -5
- package/dist/test/flparser.test.d.ts +0 -6
- package/dist/test/flparser.test.d.ts.map +0 -1
- package/dist/vitest.config.d.ts +0 -3
- package/dist/vitest.config.d.ts.map +0 -1
package/dist/index.js
CHANGED
|
@@ -3,6 +3,9 @@ var WORD = 64;
|
|
|
3
3
|
var DWORD = 128;
|
|
4
4
|
var TEXT = 192;
|
|
5
5
|
var DATA = 208;
|
|
6
|
+
var FIXED_SIZE_OVERRIDES = {
|
|
7
|
+
172: 3
|
|
8
|
+
};
|
|
6
9
|
var NEW_TEXT_IDS = [
|
|
7
10
|
TEXT + 49,
|
|
8
11
|
// ArrangementID.Name
|
|
@@ -149,6 +152,7 @@ var EVENT_ID = {
|
|
|
149
152
|
// 236
|
|
150
153
|
};
|
|
151
154
|
var EVENT_KIND = {
|
|
155
|
+
[172]: "unknown",
|
|
152
156
|
// Project events
|
|
153
157
|
[EVENT_ID.PROJECT_LOOP_ACTIVE]: "u8",
|
|
154
158
|
[EVENT_ID.PROJECT_SHOW_INFO]: "u8",
|
|
@@ -257,6 +261,10 @@ function getEventKind(eventId) {
|
|
|
257
261
|
}
|
|
258
262
|
}
|
|
259
263
|
function getEventFixedSize(eventId) {
|
|
264
|
+
const override = FIXED_SIZE_OVERRIDES[eventId];
|
|
265
|
+
if (override !== void 0) {
|
|
266
|
+
return override;
|
|
267
|
+
}
|
|
260
268
|
if (eventId < WORD) {
|
|
261
269
|
return 1;
|
|
262
270
|
} else if (eventId < DWORD) {
|
|
@@ -547,8 +555,8 @@ function parseFlp(buffer) {
|
|
|
547
555
|
throw new Error(`Invalid header size: expected ${FLP_HEADER_SIZE}, got ${headerSize}`);
|
|
548
556
|
}
|
|
549
557
|
const format = reader.readI16LE();
|
|
550
|
-
|
|
551
|
-
|
|
558
|
+
reader.readU16LE();
|
|
559
|
+
reader.readU16LE();
|
|
552
560
|
if (format < -1 || format > 80) {
|
|
553
561
|
throw new Error(`Invalid file format: ${format}`);
|
|
554
562
|
}
|
|
@@ -568,7 +576,6 @@ function parseFlp(buffer) {
|
|
|
568
576
|
const events = [];
|
|
569
577
|
let flVersion = "0.0.0";
|
|
570
578
|
let useUnicode = false;
|
|
571
|
-
let detectedVersion = false;
|
|
572
579
|
const eventsStart = reader.tell();
|
|
573
580
|
const eventsEnd = eventsStart + eventsSize;
|
|
574
581
|
while (reader.tell() < eventsEnd) {
|
|
@@ -576,14 +583,9 @@ function parseFlp(buffer) {
|
|
|
576
583
|
const eventId = reader.readU8();
|
|
577
584
|
let payloadSize;
|
|
578
585
|
let headerSize2;
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
} else if (eventId < DWORD) {
|
|
583
|
-
payloadSize = 2;
|
|
584
|
-
headerSize2 = 1;
|
|
585
|
-
} else if (eventId < TEXT) {
|
|
586
|
-
payloadSize = 4;
|
|
586
|
+
const fixedSize = getEventFixedSize(eventId);
|
|
587
|
+
if (fixedSize >= 0) {
|
|
588
|
+
payloadSize = fixedSize;
|
|
587
589
|
headerSize2 = 1;
|
|
588
590
|
} else {
|
|
589
591
|
const sizeStart = reader.tell();
|
|
@@ -600,16 +602,14 @@ function parseFlp(buffer) {
|
|
|
600
602
|
payload: Buffer.from(payload)
|
|
601
603
|
};
|
|
602
604
|
events.push(event);
|
|
603
|
-
if (eventId === EVENT_ID.PROJECT_FL_VERSION &&
|
|
605
|
+
if (eventId === EVENT_ID.PROJECT_FL_VERSION && flVersion === "0.0.0") {
|
|
604
606
|
const candidate = payload.toString("ascii").replace(/\0/g, "").trim();
|
|
605
|
-
|
|
606
|
-
if (isSemverLike) {
|
|
607
|
+
if (/^\d+(\.\d+)+$/.test(candidate)) {
|
|
607
608
|
flVersion = candidate;
|
|
608
609
|
const parts = flVersion.split(".").map((p) => parseInt(p, 10));
|
|
609
610
|
const major = parts[0] ?? 0;
|
|
610
611
|
const minor = parts[1] ?? 0;
|
|
611
612
|
useUnicode = parts.length >= 2 && (major > 11 || major === 11 && minor >= 5);
|
|
612
|
-
detectedVersion = true;
|
|
613
613
|
}
|
|
614
614
|
}
|
|
615
615
|
}
|
|
@@ -676,7 +676,7 @@ function createEvent(id, payload) {
|
|
|
676
676
|
function patchEvents(parsed, patcher) {
|
|
677
677
|
const patchedEvents = parsed.events.map((event, index) => {
|
|
678
678
|
const patched = patcher(event, index);
|
|
679
|
-
if (patched.payload !== event.payload &&
|
|
679
|
+
if (patched.payload !== event.payload && event.header.length > 1) {
|
|
680
680
|
const headerWriter = new BinaryWriter();
|
|
681
681
|
headerWriter.writeU8(patched.id);
|
|
682
682
|
headerWriter.writeVarInt(patched.payload.length);
|
|
@@ -769,62 +769,166 @@ function createNumberPayload(value, kind) {
|
|
|
769
769
|
}
|
|
770
770
|
|
|
771
771
|
// src/api/ProjectApi.ts
|
|
772
|
+
var PROJECT_EVENT_ORDER = [
|
|
773
|
+
EVENT_ID.PROJECT_TEMPO,
|
|
774
|
+
EVENT_ID.PROJECT_TITLE,
|
|
775
|
+
EVENT_ID.PROJECT_GENRE,
|
|
776
|
+
EVENT_ID.PROJECT_ARTISTS,
|
|
777
|
+
EVENT_ID.PROJECT_COMMENTS,
|
|
778
|
+
EVENT_ID.PROJECT_TIMESTAMP
|
|
779
|
+
];
|
|
780
|
+
function insertProjectEvent(parsed, event) {
|
|
781
|
+
const orderIndex = PROJECT_EVENT_ORDER.indexOf(event.id);
|
|
782
|
+
let insertionIndex = -1;
|
|
783
|
+
if (orderIndex >= 0) {
|
|
784
|
+
const laterIds = new Set(PROJECT_EVENT_ORDER.slice(orderIndex + 1));
|
|
785
|
+
insertionIndex = parsed.events.findIndex((candidate) => laterIds.has(candidate.id));
|
|
786
|
+
if (insertionIndex < 0) {
|
|
787
|
+
const previousIds = new Set(PROJECT_EVENT_ORDER.slice(0, orderIndex + 1));
|
|
788
|
+
for (let index = parsed.events.length - 1; index >= 0; index--) {
|
|
789
|
+
if (previousIds.has(parsed.events[index].id)) {
|
|
790
|
+
insertionIndex = index + 1;
|
|
791
|
+
break;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
if (insertionIndex < 0) {
|
|
797
|
+
insertionIndex = parsed.events.findIndex((candidate) => candidate.id === EVENT_ID.CHANNEL_NEW);
|
|
798
|
+
}
|
|
799
|
+
if (insertionIndex < 0) {
|
|
800
|
+
const versionIndex = parsed.events.findIndex(
|
|
801
|
+
(candidate) => candidate.id === EVENT_ID.PROJECT_FL_VERSION
|
|
802
|
+
);
|
|
803
|
+
insertionIndex = versionIndex >= 0 ? versionIndex + 1 : parsed.events.length;
|
|
804
|
+
}
|
|
805
|
+
const events = [...parsed.events];
|
|
806
|
+
events.splice(insertionIndex, 0, event);
|
|
807
|
+
return { ...parsed, events };
|
|
808
|
+
}
|
|
809
|
+
function replaceEventPayload(parsed, target, payload) {
|
|
810
|
+
return patchEvents(parsed, (event) => event === target ? { ...event, payload } : event);
|
|
811
|
+
}
|
|
812
|
+
function readBpm(parsed) {
|
|
813
|
+
const tempoEvent = findFirstEvent(parsed, EVENT_ID.PROJECT_TEMPO);
|
|
814
|
+
if (tempoEvent) {
|
|
815
|
+
return getEventNumber(tempoEvent) / 1e3;
|
|
816
|
+
}
|
|
817
|
+
const coarseEvent = findFirstEvent(parsed, EVENT_ID.PROJECT__TEMPO_COARSE);
|
|
818
|
+
if (coarseEvent) {
|
|
819
|
+
let bpm = coarseEvent.payload.readUInt16LE(0);
|
|
820
|
+
const fineEvent = findFirstEvent(parsed, EVENT_ID.PROJECT__TEMPO_FINE);
|
|
821
|
+
if (fineEvent) {
|
|
822
|
+
bpm += fineEvent.payload.readUInt16LE(0) / 1e3;
|
|
823
|
+
}
|
|
824
|
+
return bpm;
|
|
825
|
+
}
|
|
826
|
+
return null;
|
|
827
|
+
}
|
|
772
828
|
function readProjectMeta(parsed) {
|
|
773
829
|
const titleEvent = findFirstEvent(parsed, EVENT_ID.PROJECT_TITLE);
|
|
774
830
|
const commentsEvent = findFirstEvent(parsed, EVENT_ID.PROJECT_COMMENTS);
|
|
775
831
|
const artistsEvent = findFirstEvent(parsed, EVENT_ID.PROJECT_ARTISTS);
|
|
776
832
|
const genreEvent = findFirstEvent(parsed, EVENT_ID.PROJECT_GENRE);
|
|
777
|
-
const tempoEvent = findFirstEvent(parsed, EVENT_ID.PROJECT_TEMPO);
|
|
778
833
|
return {
|
|
779
834
|
name: titleEvent ? getEventString(titleEvent, parsed.useUnicode) : null,
|
|
780
835
|
description: commentsEvent ? getEventString(commentsEvent, parsed.useUnicode) : null,
|
|
781
836
|
artist: artistsEvent ? getEventString(artistsEvent, parsed.useUnicode) : null,
|
|
782
837
|
genre: genreEvent ? getEventString(genreEvent, parsed.useUnicode) : null,
|
|
783
|
-
bpm:
|
|
838
|
+
bpm: readBpm(parsed)
|
|
784
839
|
};
|
|
785
840
|
}
|
|
841
|
+
function compareVersion(version, expected) {
|
|
842
|
+
const actual = version.split(".").map((part) => Number.parseInt(part, 10));
|
|
843
|
+
const length = Math.max(actual.length, expected.length);
|
|
844
|
+
for (let index = 0; index < length; index++) {
|
|
845
|
+
const difference = (actual[index] ?? 0) - (expected[index] ?? 0);
|
|
846
|
+
if (difference !== 0) return Math.sign(difference);
|
|
847
|
+
}
|
|
848
|
+
return 0;
|
|
849
|
+
}
|
|
850
|
+
function validateBpm(parsed, bpm) {
|
|
851
|
+
if (!Number.isFinite(bpm)) {
|
|
852
|
+
throw new RangeError("BPM must be a finite number");
|
|
853
|
+
}
|
|
854
|
+
const legacyHighTempo = compareVersion(parsed.flVersion, [1, 4, 2]) >= 0 && compareVersion(parsed.flVersion, [11]) < 0;
|
|
855
|
+
const maximum = legacyHighTempo ? 999 : 522;
|
|
856
|
+
if (bpm < 10 || bpm > maximum) {
|
|
857
|
+
throw new RangeError(`BPM must be between 10 and ${maximum} for FL Studio ${parsed.flVersion}`);
|
|
858
|
+
}
|
|
859
|
+
if (parsed.flVersion !== "0.0.0" && compareVersion(parsed.flVersion, [3, 4]) < 0 && !Number.isInteger(bpm)) {
|
|
860
|
+
throw new TypeError(`FL Studio ${parsed.flVersion} does not support decimal BPM values`);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
function writeTextProjectEvent(parsed, eventId, value) {
|
|
864
|
+
if (value === void 0) return parsed;
|
|
865
|
+
const payload = createTextPayload(value ?? "", parsed.useUnicode);
|
|
866
|
+
const existing = findFirstEvent(parsed, eventId);
|
|
867
|
+
return existing ? replaceEventPayload(parsed, existing, payload) : insertProjectEvent(parsed, createEvent(eventId, payload));
|
|
868
|
+
}
|
|
869
|
+
function writeProjectBpm(parsed, bpm) {
|
|
870
|
+
if (bpm === void 0 || bpm === null) return parsed;
|
|
871
|
+
validateBpm(parsed, bpm);
|
|
872
|
+
const scaledBpm = Math.round(bpm * 1e3);
|
|
873
|
+
const coarseBpm = Math.floor(scaledBpm / 1e3);
|
|
874
|
+
const fineBpm = scaledBpm % 1e3;
|
|
875
|
+
const modernEvent = findFirstEvent(parsed, EVENT_ID.PROJECT_TEMPO);
|
|
876
|
+
const coarseEvent = findFirstEvent(parsed, EVENT_ID.PROJECT__TEMPO_COARSE);
|
|
877
|
+
const fineEvent = findFirstEvent(parsed, EVENT_ID.PROJECT__TEMPO_FINE);
|
|
878
|
+
let result = parsed;
|
|
879
|
+
if (modernEvent) {
|
|
880
|
+
result = replaceEventPayload(result, modernEvent, createNumberPayload(scaledBpm, "u32"));
|
|
881
|
+
}
|
|
882
|
+
if (coarseEvent) {
|
|
883
|
+
result = replaceEventPayload(result, coarseEvent, createNumberPayload(coarseBpm, "u16"));
|
|
884
|
+
}
|
|
885
|
+
if (fineEvent) {
|
|
886
|
+
result = replaceEventPayload(result, fineEvent, createNumberPayload(fineBpm, "u16"));
|
|
887
|
+
}
|
|
888
|
+
if (!modernEvent && !coarseEvent && !fineEvent) {
|
|
889
|
+
result = insertProjectEvent(
|
|
890
|
+
result,
|
|
891
|
+
createEvent(EVENT_ID.PROJECT_TEMPO, createNumberPayload(scaledBpm, "u32"))
|
|
892
|
+
);
|
|
893
|
+
} else if (!modernEvent && coarseEvent && !fineEvent && fineBpm > 0) {
|
|
894
|
+
const updatedCoarse = findFirstEvent(result, EVENT_ID.PROJECT__TEMPO_COARSE);
|
|
895
|
+
const index = result.events.indexOf(updatedCoarse) + 1;
|
|
896
|
+
const events = [...result.events];
|
|
897
|
+
events.splice(
|
|
898
|
+
index,
|
|
899
|
+
0,
|
|
900
|
+
createEvent(EVENT_ID.PROJECT__TEMPO_FINE, createNumberPayload(fineBpm, "u16"))
|
|
901
|
+
);
|
|
902
|
+
result = { ...result, events };
|
|
903
|
+
} else if (!modernEvent && !coarseEvent && fineEvent) {
|
|
904
|
+
const updatedFine = findFirstEvent(result, EVENT_ID.PROJECT__TEMPO_FINE);
|
|
905
|
+
const index = result.events.indexOf(updatedFine);
|
|
906
|
+
const events = [...result.events];
|
|
907
|
+
events.splice(
|
|
908
|
+
index,
|
|
909
|
+
0,
|
|
910
|
+
createEvent(EVENT_ID.PROJECT__TEMPO_COARSE, createNumberPayload(coarseBpm, "u16"))
|
|
911
|
+
);
|
|
912
|
+
result = { ...result, events };
|
|
913
|
+
}
|
|
914
|
+
return result;
|
|
915
|
+
}
|
|
786
916
|
function writeProjectMeta(parsed, meta) {
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
if (meta.description !== void 0 && event.id === EVENT_ID.PROJECT_COMMENTS) {
|
|
795
|
-
return {
|
|
796
|
-
...event,
|
|
797
|
-
payload: createTextPayload(meta.description ?? "", parsed.useUnicode)
|
|
798
|
-
};
|
|
799
|
-
}
|
|
800
|
-
if (meta.artist !== void 0 && event.id === EVENT_ID.PROJECT_ARTISTS) {
|
|
801
|
-
return {
|
|
802
|
-
...event,
|
|
803
|
-
payload: createTextPayload(meta.artist ?? "", parsed.useUnicode)
|
|
804
|
-
};
|
|
805
|
-
}
|
|
806
|
-
if (meta.genre !== void 0 && event.id === EVENT_ID.PROJECT_GENRE) {
|
|
807
|
-
return {
|
|
808
|
-
...event,
|
|
809
|
-
payload: createTextPayload(meta.genre ?? "", parsed.useUnicode)
|
|
810
|
-
};
|
|
811
|
-
}
|
|
812
|
-
if (meta.bpm && Math.abs(meta.bpm) > 0 && event.id === EVENT_ID.PROJECT_TEMPO) {
|
|
813
|
-
return {
|
|
814
|
-
...event,
|
|
815
|
-
payload: createNumberPayload(Math.round(Math.abs(meta.bpm) * 1e3), "u32")
|
|
816
|
-
};
|
|
817
|
-
}
|
|
818
|
-
return event;
|
|
819
|
-
});
|
|
917
|
+
let result = parsed;
|
|
918
|
+
result = writeProjectBpm(result, meta.bpm);
|
|
919
|
+
result = writeTextProjectEvent(result, EVENT_ID.PROJECT_TITLE, meta.name);
|
|
920
|
+
result = writeTextProjectEvent(result, EVENT_ID.PROJECT_GENRE, meta.genre);
|
|
921
|
+
result = writeTextProjectEvent(result, EVENT_ID.PROJECT_ARTISTS, meta.artist);
|
|
922
|
+
result = writeTextProjectEvent(result, EVENT_ID.PROJECT_COMMENTS, meta.description);
|
|
923
|
+
return result;
|
|
820
924
|
}
|
|
821
|
-
var
|
|
925
|
+
var DELPHI_EPOCH_MS = Date.UTC(1899, 11, 30);
|
|
822
926
|
function delphiToDate(days) {
|
|
823
927
|
const ms = days * 24 * 60 * 60 * 1e3;
|
|
824
|
-
return new Date(
|
|
928
|
+
return new Date(DELPHI_EPOCH_MS + ms);
|
|
825
929
|
}
|
|
826
930
|
function dateToDelphiDays(date) {
|
|
827
|
-
const ms = date.getTime() -
|
|
931
|
+
const ms = date.getTime() - DELPHI_EPOCH_MS;
|
|
828
932
|
return ms / (24 * 60 * 60 * 1e3);
|
|
829
933
|
}
|
|
830
934
|
function readProjectTimeInfo(parsed) {
|
|
@@ -845,31 +949,23 @@ function readProjectTimeInfo(parsed) {
|
|
|
845
949
|
};
|
|
846
950
|
}
|
|
847
951
|
function writeProjectTimeInfo(parsed, info) {
|
|
952
|
+
if (info.creationDate === void 0 && info.workTimeSeconds === void 0) {
|
|
953
|
+
return parsed;
|
|
954
|
+
}
|
|
848
955
|
const current = readProjectTimeInfo(parsed);
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
writer.writeF64LE(info.workTimeSeconds / (24 * 60 * 60));
|
|
863
|
-
} else if (current.workTimeSeconds !== null) {
|
|
864
|
-
writer.writeF64LE(current.workTimeSeconds / (24 * 60 * 60));
|
|
865
|
-
} else {
|
|
866
|
-
writer.writeF64LE(0);
|
|
867
|
-
}
|
|
868
|
-
return {
|
|
869
|
-
...event,
|
|
870
|
-
payload: writer.toBuffer()
|
|
871
|
-
};
|
|
872
|
-
});
|
|
956
|
+
if (info.creationDate !== void 0 && info.creationDate !== null && Number.isNaN(info.creationDate.getTime())) {
|
|
957
|
+
throw new RangeError("creationDate must be a valid Date");
|
|
958
|
+
}
|
|
959
|
+
if (info.workTimeSeconds !== void 0 && info.workTimeSeconds !== null && (!Number.isFinite(info.workTimeSeconds) || info.workTimeSeconds < 0)) {
|
|
960
|
+
throw new RangeError("workTimeSeconds must be a finite, non-negative number");
|
|
961
|
+
}
|
|
962
|
+
const creationDateDays = info.creationDate === null ? 0 : info.creationDate !== void 0 ? dateToDelphiDays(info.creationDate) : current.creationDate !== null ? dateToDelphiDays(current.creationDate) : 0;
|
|
963
|
+
const workTimeDays = info.workTimeSeconds === null ? 0 : info.workTimeSeconds !== void 0 ? info.workTimeSeconds / (24 * 60 * 60) : current.workTimeSeconds !== null ? current.workTimeSeconds / (24 * 60 * 60) : 0;
|
|
964
|
+
const writer = new BinaryWriter();
|
|
965
|
+
writer.writeF64LE(creationDateDays);
|
|
966
|
+
writer.writeF64LE(workTimeDays);
|
|
967
|
+
const timestampEvent = findFirstEvent(parsed, EVENT_ID.PROJECT_TIMESTAMP);
|
|
968
|
+
return timestampEvent ? replaceEventPayload(parsed, timestampEvent, writer.toBuffer()) : insertProjectEvent(parsed, createEvent(EVENT_ID.PROJECT_TIMESTAMP, writer.toBuffer()));
|
|
873
969
|
}
|
|
874
970
|
function listSamples(parsed) {
|
|
875
971
|
const samples = [];
|