ts-flp 1.0.5 → 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 +154 -147
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +154 -147
- package/dist/index.js.map +1 -1
- package/dist/src/api/ProjectApi.d.ts.map +1 -1
- package/dist/src/generated/events.generated.d.ts.map +1 -1
- package/dist/src/parser/FlpParser.d.ts +2 -6
- package/dist/src/parser/FlpParser.d.ts.map +1 -1
- package/package.json +7 -5
- package/dist/test/dump-all-info.d.ts +0 -2
- package/dist/test/dump-all-info.d.ts.map +0 -1
- 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) {
|
|
@@ -533,62 +541,9 @@ function varIntSize(value) {
|
|
|
533
541
|
}
|
|
534
542
|
|
|
535
543
|
// src/parser/FlpParser.ts
|
|
536
|
-
var KNOWN_DWORD_IDS = new Set(
|
|
537
|
-
Object.keys(EVENT_KIND).map(Number).filter((id) => id >= DWORD && id < TEXT)
|
|
538
|
-
);
|
|
539
544
|
var FLP_HEADER_MAGIC = "FLhd";
|
|
540
545
|
var FLP_DATA_MAGIC = "FLdt";
|
|
541
546
|
var FLP_HEADER_SIZE = 6;
|
|
542
|
-
var LOOKAHEAD_BYTES = 200;
|
|
543
|
-
var LOOKAHEAD_VARINT_MAX = 1e5;
|
|
544
|
-
function readVarIntRaw(buf, off, end) {
|
|
545
|
-
let shift = 0;
|
|
546
|
-
let size = 0;
|
|
547
|
-
const start = off;
|
|
548
|
-
while (true) {
|
|
549
|
-
if (off >= end) return { size: -1, bytesRead: 0 };
|
|
550
|
-
const byte = buf[off];
|
|
551
|
-
off++;
|
|
552
|
-
size |= (byte & 127) << shift;
|
|
553
|
-
shift += 7;
|
|
554
|
-
if (!(byte & 128)) break;
|
|
555
|
-
}
|
|
556
|
-
return { size, bytesRead: off - start };
|
|
557
|
-
}
|
|
558
|
-
function scoreAlignment(buf, off, end, bytesToCheck) {
|
|
559
|
-
const limit = Math.min(off + bytesToCheck, end);
|
|
560
|
-
let textDataCount = 0;
|
|
561
|
-
let consecutiveSmallByte = 0;
|
|
562
|
-
let maxConsecutiveSmallByte = 0;
|
|
563
|
-
while (off < limit) {
|
|
564
|
-
const id = buf[off];
|
|
565
|
-
if (id < WORD) {
|
|
566
|
-
off += 2;
|
|
567
|
-
if (id < 32 && !KNOWN_DWORD_IDS.has(id)) {
|
|
568
|
-
consecutiveSmallByte++;
|
|
569
|
-
maxConsecutiveSmallByte = Math.max(maxConsecutiveSmallByte, consecutiveSmallByte);
|
|
570
|
-
} else {
|
|
571
|
-
consecutiveSmallByte = 0;
|
|
572
|
-
}
|
|
573
|
-
} else if (id < DWORD) {
|
|
574
|
-
off += 3;
|
|
575
|
-
consecutiveSmallByte = 0;
|
|
576
|
-
} else if (id < TEXT) {
|
|
577
|
-
off += 5;
|
|
578
|
-
consecutiveSmallByte = 0;
|
|
579
|
-
} else {
|
|
580
|
-
const vi = readVarIntRaw(buf, off + 1, end);
|
|
581
|
-
if (vi.size >= 0 && vi.size < LOOKAHEAD_VARINT_MAX && off + 1 + vi.bytesRead + vi.size <= end) {
|
|
582
|
-
textDataCount++;
|
|
583
|
-
off = off + 1 + vi.bytesRead + vi.size;
|
|
584
|
-
} else {
|
|
585
|
-
return -100;
|
|
586
|
-
}
|
|
587
|
-
consecutiveSmallByte = 0;
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
return textDataCount * 10 - maxConsecutiveSmallByte * 3;
|
|
591
|
-
}
|
|
592
547
|
function parseFlp(buffer) {
|
|
593
548
|
const reader = new BinaryReader(buffer);
|
|
594
549
|
const headerMagic = reader.readBytes(4).toString("ascii");
|
|
@@ -600,8 +555,8 @@ function parseFlp(buffer) {
|
|
|
600
555
|
throw new Error(`Invalid header size: expected ${FLP_HEADER_SIZE}, got ${headerSize}`);
|
|
601
556
|
}
|
|
602
557
|
const format = reader.readI16LE();
|
|
603
|
-
|
|
604
|
-
|
|
558
|
+
reader.readU16LE();
|
|
559
|
+
reader.readU16LE();
|
|
605
560
|
if (format < -1 || format > 80) {
|
|
606
561
|
throw new Error(`Invalid file format: ${format}`);
|
|
607
562
|
}
|
|
@@ -628,39 +583,10 @@ function parseFlp(buffer) {
|
|
|
628
583
|
const eventId = reader.readU8();
|
|
629
584
|
let payloadSize;
|
|
630
585
|
let headerSize2;
|
|
631
|
-
|
|
632
|
-
|
|
586
|
+
const fixedSize = getEventFixedSize(eventId);
|
|
587
|
+
if (fixedSize >= 0) {
|
|
588
|
+
payloadSize = fixedSize;
|
|
633
589
|
headerSize2 = 1;
|
|
634
|
-
} else if (eventId < DWORD) {
|
|
635
|
-
payloadSize = 2;
|
|
636
|
-
headerSize2 = 1;
|
|
637
|
-
} else if (eventId < TEXT) {
|
|
638
|
-
if (KNOWN_DWORD_IDS.has(eventId)) {
|
|
639
|
-
payloadSize = 4;
|
|
640
|
-
headerSize2 = 1;
|
|
641
|
-
} else {
|
|
642
|
-
const afterDword = eventStart + 1 + 4;
|
|
643
|
-
const vi = readVarIntRaw(buffer, eventStart + 1, eventsEnd);
|
|
644
|
-
if (vi.size < 0 || vi.size > LOOKAHEAD_VARINT_MAX || eventStart + 1 + vi.bytesRead + vi.size > eventsEnd) {
|
|
645
|
-
payloadSize = 4;
|
|
646
|
-
headerSize2 = 1;
|
|
647
|
-
} else if (vi.size === 3) {
|
|
648
|
-
payloadSize = 4;
|
|
649
|
-
headerSize2 = 1;
|
|
650
|
-
} else {
|
|
651
|
-
const afterVarInt = eventStart + 1 + vi.bytesRead + vi.size;
|
|
652
|
-
const dwordScore = scoreAlignment(buffer, afterDword, eventsEnd, LOOKAHEAD_BYTES);
|
|
653
|
-
const varIntScore = scoreAlignment(buffer, afterVarInt, eventsEnd, LOOKAHEAD_BYTES);
|
|
654
|
-
if (varIntScore > dwordScore + 2) {
|
|
655
|
-
payloadSize = vi.size;
|
|
656
|
-
reader.seek(eventStart + 1 + vi.bytesRead);
|
|
657
|
-
headerSize2 = 1 + vi.bytesRead;
|
|
658
|
-
} else {
|
|
659
|
-
payloadSize = 4;
|
|
660
|
-
headerSize2 = 1;
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
590
|
} else {
|
|
665
591
|
const sizeStart = reader.tell();
|
|
666
592
|
payloadSize = reader.readVarInt();
|
|
@@ -843,6 +769,46 @@ function createNumberPayload(value, kind) {
|
|
|
843
769
|
}
|
|
844
770
|
|
|
845
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
|
+
}
|
|
846
812
|
function readBpm(parsed) {
|
|
847
813
|
const tempoEvent = findFirstEvent(parsed, EVENT_ID.PROJECT_TEMPO);
|
|
848
814
|
if (tempoEvent) {
|
|
@@ -872,48 +838,97 @@ function readProjectMeta(parsed) {
|
|
|
872
838
|
bpm: readBpm(parsed)
|
|
873
839
|
};
|
|
874
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
|
+
}
|
|
875
916
|
function writeProjectMeta(parsed, meta) {
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
if (meta.description !== void 0 && event.id === EVENT_ID.PROJECT_COMMENTS) {
|
|
884
|
-
return {
|
|
885
|
-
...event,
|
|
886
|
-
payload: createTextPayload(meta.description ?? "", parsed.useUnicode)
|
|
887
|
-
};
|
|
888
|
-
}
|
|
889
|
-
if (meta.artist !== void 0 && event.id === EVENT_ID.PROJECT_ARTISTS) {
|
|
890
|
-
return {
|
|
891
|
-
...event,
|
|
892
|
-
payload: createTextPayload(meta.artist ?? "", parsed.useUnicode)
|
|
893
|
-
};
|
|
894
|
-
}
|
|
895
|
-
if (meta.genre !== void 0 && event.id === EVENT_ID.PROJECT_GENRE) {
|
|
896
|
-
return {
|
|
897
|
-
...event,
|
|
898
|
-
payload: createTextPayload(meta.genre ?? "", parsed.useUnicode)
|
|
899
|
-
};
|
|
900
|
-
}
|
|
901
|
-
if (meta.bpm && Math.abs(meta.bpm) > 0 && event.id === EVENT_ID.PROJECT_TEMPO) {
|
|
902
|
-
return {
|
|
903
|
-
...event,
|
|
904
|
-
payload: createNumberPayload(Math.round(Math.abs(meta.bpm) * 1e3), "u32")
|
|
905
|
-
};
|
|
906
|
-
}
|
|
907
|
-
return event;
|
|
908
|
-
});
|
|
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;
|
|
909
924
|
}
|
|
910
|
-
var
|
|
925
|
+
var DELPHI_EPOCH_MS = Date.UTC(1899, 11, 30);
|
|
911
926
|
function delphiToDate(days) {
|
|
912
927
|
const ms = days * 24 * 60 * 60 * 1e3;
|
|
913
|
-
return new Date(
|
|
928
|
+
return new Date(DELPHI_EPOCH_MS + ms);
|
|
914
929
|
}
|
|
915
930
|
function dateToDelphiDays(date) {
|
|
916
|
-
const ms = date.getTime() -
|
|
931
|
+
const ms = date.getTime() - DELPHI_EPOCH_MS;
|
|
917
932
|
return ms / (24 * 60 * 60 * 1e3);
|
|
918
933
|
}
|
|
919
934
|
function readProjectTimeInfo(parsed) {
|
|
@@ -934,31 +949,23 @@ function readProjectTimeInfo(parsed) {
|
|
|
934
949
|
};
|
|
935
950
|
}
|
|
936
951
|
function writeProjectTimeInfo(parsed, info) {
|
|
952
|
+
if (info.creationDate === void 0 && info.workTimeSeconds === void 0) {
|
|
953
|
+
return parsed;
|
|
954
|
+
}
|
|
937
955
|
const current = readProjectTimeInfo(parsed);
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
writer.writeF64LE(info.workTimeSeconds / (24 * 60 * 60));
|
|
952
|
-
} else if (current.workTimeSeconds !== null) {
|
|
953
|
-
writer.writeF64LE(current.workTimeSeconds / (24 * 60 * 60));
|
|
954
|
-
} else {
|
|
955
|
-
writer.writeF64LE(0);
|
|
956
|
-
}
|
|
957
|
-
return {
|
|
958
|
-
...event,
|
|
959
|
-
payload: writer.toBuffer()
|
|
960
|
-
};
|
|
961
|
-
});
|
|
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()));
|
|
962
969
|
}
|
|
963
970
|
function listSamples(parsed) {
|
|
964
971
|
const samples = [];
|