-
Notifications
You must be signed in to change notification settings - Fork 26
/
main.js
2034 lines (1913 loc) · 91.6 KB
/
main.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
//@ts-check
try {
//Rhino的const是全局作用域, 会报错!
var { requireShared } = require("./src/requireShared.js");
/**
* @type {import("../shared/getPosInteractive.js")}
*/
var getPosInteractive = requireShared("getPosInteractive.js");
/**
* @type {import("../shared/runtimes.js")}
*/
var runtimes = requireShared("runtimes.js");
var MusicFormats = require("./src/musicFormats.js");
var MidiDeviceManager = require("./src/midiDeviceManager.js");
var GameProfile = require("./src/gameProfile.js");
var Visualizer = require("./src/visualizer.js");
var FileChooser = require("./src/fileChooser.js");
var Players = require("./src/players.js");
var configuration = require("./src/configuration.js");
var PassManager = require("./src/passManager.js");
var midiPitch = require("./src/midiPitch.js");
var noteUtils = require("./src/noteUtils.js");
} catch (e) {
toast("请不要单独下载/复制这个脚本,需要下载'楚留香音乐盒'中的所有文件!");
toast("模块加载错误");
toast(e);
console.error(e);
}
const musicDir = configuration.getMusicDir();
const scriptVersion = 25;
//如果遇到奇怪的问题, 可以将下面这行代码前面两个斜杠去掉, 之后再次运行脚本, 即可清除当前的配置文件。
//setGlobalConfig("userGameProfile", null);
//在日志中打印脚本生成的中间结果, 可选项: parse, humanify, key, timing, merge, gestures
const debugDumpPass = "";
//将两个/几个彼此间隔时间小于以下阈值的音符合并, 单位: 秒
//用于自动演奏的合并阈值
const autoPlayMergeThreshold = 0.01;
//用于乐谱导出的合并阈值
const scoreExportMergeThreshold = 0.2;
//应用名称, 稍后会被初始化
let appName = undefined;
let musicFormats = new MusicFormats();
let gameProfile = new GameProfile();
let visualizer = new Visualizer();
const setGlobalConfig = configuration.setGlobalConfig;
const readGlobalConfig = configuration.readGlobalConfig;
const haveFileConfig = configuration.haveFileConfig;
const setFileConfig = configuration.setFileConfig;
const readFileConfig = configuration.readFileConfig;
/**
* @brief 导出数据的格式类型
* @enum {string}
*/
const ScoreExportType = {
none: "none",
keyboardScore: "keyboardScore",
keySequenceJSON: "keySequenceJSON",
};
/**
* @enum {string}
*/
const ScriptOperationMode = {
NotRunning: "NotRunning",
FilePlayer: "FilePlayer",
MIDIInputStreaming: "MIDIInputStreaming",
};
/**
* @brief 加载配置文件
*/
function loadConfiguration() {
try {
// TODO: 自定义配置
let userGameProfile = readGlobalConfig("userGameProfile", null);
if (userGameProfile != null) {
gameProfile.loadGameConfigs(userGameProfile);
} else {
gameProfile.loadDefaultGameConfigs();
}
let lastConfigName = readGlobalConfig("lastConfigName", "");
//尝试加载用户设置的游戏配置
let activeConfigName = readGlobalConfig("activeConfigName", null);
let res = gameProfile.setConfigByName(activeConfigName);
if (res == false) {
console.log("尝试加载用户设置的游戏配置...失败!");
} else {
console.log("尝试加载用户设置的游戏配置...成功, 当前配置: " + gameProfile.getCurrentConfigTypeName());
}
//尝试通过包名加载游戏配置 (加载失败后保留当前配置)
if (auto.service != null) {
let currentPackageName = currentPackage();
console.log("当前包名:" + currentPackageName);
res = gameProfile.setConfigByPackageName(currentPackageName);
if (res == false) {
console.log("尝试通过包名加载游戏配置...失败!");
} else {
console.log("尝试通过包名加载游戏配置...成功, 当前配置: " + gameProfile.getCurrentConfigTypeName());
//保存当前配置
setGlobalConfig("activeConfigName", gameProfile.getCurrentConfigTypeName());
}
}else{
console.log("未启用无障碍服务, 跳过尝试通过包名加载游戏配置");
}
if (gameProfile.getCurrentConfig() == null) {
console.error("未找到合适配置, 已加载默认配置!");
toast("未找到合适配置, 已加载默认配置!");
gameProfile.setConfigByName("楚留香");
}
if (lastConfigName != gameProfile.getCurrentConfigTypeName()) {
//如果配置发生了变化, 则清空上次的变体与键位配置
setGlobalConfig("lastConfigName", gameProfile.getCurrentConfigTypeName());
setGlobalConfig("lastVariantName", "");
setGlobalConfig("lastKeyTypeName", "");
}
//加载变体配置和键位配置
let lastVariantName = readGlobalConfig("lastVariantName", "");
if (lastVariantName != "") {
let res = gameProfile.setCurrentVariantByTypeName(lastVariantName);
if (res == false) {
console.log("尝试加载用户设置的变体配置...失败!");
gameProfile.setCurrentVariantDefault();
} else {
console.log("尝试加载用户设置的变体配置...成功");
}
} else {
gameProfile.setCurrentVariantDefault();
console.log("游戏配置发生变化, 已加载默认变体配置");
}
setGlobalConfig("lastVariantName", gameProfile.getCurrentVariantTypeName());
let lastKeyTypeName = readGlobalConfig("lastKeyTypeName", "");
if (lastKeyTypeName != "") {
let res = gameProfile.setCurrentKeyLayoutByTypeName(lastKeyTypeName);
if (res == false) {
console.log("尝试加载用户设置的键位配置...失败!");
gameProfile.setCurrentKeyLayoutDefault();
} else {
console.log("尝试加载用户设置的键位配置...成功");
}
} else {
gameProfile.setCurrentKeyLayoutDefault();
console.log("游戏配置发生变化, 已加载默认键位配置");
}
setGlobalConfig("lastKeyTypeName", gameProfile.getCurrentKeyLayoutTypeName());
} catch (error) {
toastLog("加载配置文件失败! 已自动加载默认配置!");
console.warn(error);
gameProfile.loadDefaultGameConfigs();
setGlobalConfig("userGameProfile", null);
}
}
function getFileList() {
return files.listDir(musicDir, function (name) {
return files.isFile(files.join(musicDir, name)) && musicFormats.isMusicFile(name);
});
}
/**
* 启动midi串流
* @returns {{
* onDataReceived: (callback: (data: Array<Uint8Array>) => void) => void,
* close: () => void,
* } | null}
*/
function setupMidiStream() {
const midiEvt = events.emitter(threads.currentThread());
/** @type {MidiDeviceManager} */
//@ts-ignore
let midi = null;
const midiThread = threads.start(function () {
setInterval(function(){}, 1000);
midi = new MidiDeviceManager();
});
midiThread.waitFor();
while (midi == null) {
sleep(100);
}
let devNames = [];
while (1) {
devNames = midi.getMidiDeviceNames();
if (devNames.length == 0) {
if (!dialogs.confirm("错误", "没有找到MIDI设备, 点击确定重试, 点击取消退出")) {
return null;
}
} else {
break;
}
}
let deviceIndex = dialogs.select("选择MIDI设备", devNames);
if (deviceIndex == -1) {
toast("您取消了选择");
return null;
}
let portNames = midi.getMidiPortNames(deviceIndex);
if (portNames.length == 0) {
dialogs.alert("错误", "此MIDI设备没有可用的端口");
return null;
}
let portIndex = 0;
if (portNames.length > 1) { // 不太可能出现
portIndex = /** @type {Number} */ (dialogs.select("选择MIDI端口", portNames)) ;
if (portIndex == -1) {
toast("您取消了选择");
return null;
}
}
midiThread.setImmediate(() => {
midi.openDevicePort(deviceIndex, portIndex);
midi.setDataReceivedCallback(() => {
midiEvt.emit("dataReceived");
});
});
let _onDataReceived = (data) => { };
midiEvt.on("dataReceived", () => {
let keyList = [];
if (!midi.dataAvailable()) {
return;
}
while (midi.dataAvailable()) {
_onDataReceived(midi.readAll());
}
});
return {
onDataReceived: (callback) => {
_onDataReceived = callback;
},
close: () => {
midi.close();
midiThread.interrupt();
}
}
}
/**
* @brief 移除空的音轨
* @param {MusicFormats.TracksData} tracksData
* @return {MusicFormats.TracksData} 移除空的音轨后的音轨数据
*/
function removeEmptyTracks(tracksData) {
if (!tracksData.haveMultipleTrack) return tracksData;
for (let i = tracksData.tracks.length - 1; i >= 0; i--) {
if (tracksData.tracks[i].noteCount == 0) {
tracksData.tracks.splice(i, 1);
}
}
tracksData.trackCount = tracksData.tracks.length;
if (tracksData.trackCount == 1) tracksData.haveMultipleTrack = false;
return tracksData;
}
function checkEnableAccessbility() {
//启动无障碍服务
console.verbose("等待无障碍服务..");
//toast("请允许本应用的无障碍权限");
if(auto.service == null){
toastLog(`请打开应用 "${appName}" 的无障碍权限!`);
auto.waitFor();
toastLog(`无障碍权限已开启!, 请回到游戏重新点击播放`);
return false;
}
console.verbose("无障碍服务已启动");
return true;
}
/**
* @param {noteUtils.PackedNoteLike[]} noteData 音符数据
* @param {ScoreExportType} exportType 导出类型
* @brief 导出音符数据
*/
function exportNoteDataInteractive(noteData, exportType) {
switch (exportType) {
case ScoreExportType.keyboardScore:
let maxDelayTime = 0;
let confirmed = false;
let gapTime = 0;
while (!confirmed) {
gapTime = dialogs.input("输入在你打算把两个音符分到两小段的时候,它们间的时间差(单位:毫秒)", maxDelayTime.toString());
if (gapTime < 10) dialogs.alert("", "输入无效,请重新输入");
let segmentCnt = 1;
noteData.forEach(key => {
if (key[1] >= gapTime) segmentCnt++;
});
confirmed = /** @type {Boolean} */ (dialogs.confirm("", "乐谱将分为" + segmentCnt.toString() + "个小段,是否满意?")) ;
}
let toneStr = null;
switch (dialogs.select("选择导出格式", ["楚留香(键盘)", "原神(键盘)", "_简谱_"])) {
case 0:
if(gameProfile.getCurrentKeyLayoutTypeName() !== "generic_3x7"){
dialogs.alert("错误", "当前选择的游戏键位和导出格式不匹配, 请选择3x7键位");
return;
}
toneStr = "ZXCVBNMASDFGHJQWERTYU";
break;
case 1:
if(gameProfile.getCurrentKeyLayoutTypeName() !== "generic_3x7"){
dialogs.alert("错误", "当前选择的游戏键位和导出格式不匹配, 请选择3x7键位");
return;
}
toneStr = "ZXCVBNMASDFGHJQWERTYU";
break;
case 2:
if(gameProfile.getCurrentKeyLayoutTypeName() !== "generic_3x7"){
dialogs.alert("错误", "当前选择的游戏键位和导出格式不匹配, 请选择3x7键位");
return;
}
toneStr = "₁₂₃₄₅₆₇1234567¹²³⁴⁵⁶⁷"; //TODO: 这里的简谱格式可能需要调整
}
//开始转换
let outPutStr = "";
noteData.forEach(key => {
if (key[0].length > 1) {
//从高音到低音排序
key[0].sort((a, b) => {
return b - a;
});
outPutStr += "(";
key[0].forEach(element => {
outPutStr += toneStr[element];
});
outPutStr += ")";
} else {
outPutStr += toneStr[key[0][0]];
}
if (key[1] >= gapTime) outPutStr += " ";
});
//导出到文件
let baseName = "乐谱导出";
let path = musicDir + baseName + ".txt";
let i = 1;
while (files.exists(path)) {
console.log("路径 " + path + " 已存在");
path = musicDir + baseName + "(" + i.toString() + ")" + ".txt";
i++;
}
files.write(path, outPutStr);
dialogs.alert("导出成功", "已导出至" + path);
console.log("导出成功: " + path);
break;
case ScoreExportType.keySequenceJSON:
let baseName2 = "dump";
let path2 = musicDir + baseName2 + ".json";
let i2 = 1;
while (files.exists(path2)) {
console.log("路径 " + path2 + " 已存在");
path2 = musicDir + baseName2 + "(" + i2.toString() + ")" + ".json";
i2++;
}
files.write(path2, JSON.stringify(noteData));
dialogs.alert("导出成功", "已导出至" + path2);
console.log("导出成功: " + path2);
break;
default:
dialogs.alert("导出失败", "未知的导出类型");
}
}
/**
* @param {number} timeSec
*/
function sec2timeStr(timeSec) {
let minuteStr = Math.floor(timeSec / 60).toString();
let secondStr = Math.floor(timeSec % 60).toString();
if (minuteStr.length == 1) minuteStr = "0" + minuteStr;
if (secondStr.length == 1) secondStr = "0" + secondStr;
return minuteStr + ":" + secondStr;
}
function saveUserGameProfile() {
let profile = gameProfile.getGameConfigs();
setGlobalConfig("userGameProfile", profile);
console.log("保存用户游戏配置成功");
toast("保存用户游戏配置成功");
};
function debugDump(obj, name) {
console.log("====================" + name + "====================");
console.log("Type of " + name + ": " + Object.prototype.toString.call(obj));
let tmp = JSON.stringify(obj);
console.log(tmp);
console.log("====================" + name + "====================");
}
function importFileFromFileChooser() {
let fileChooser = new FileChooser();
// let filePath = fileChooser.chooseFileSync();
// if (filePath == null) {
// toast("未选择文件");
// console.warn("未选择文件");
// return;
// }
// let isMusicFile = musicFormats.isMusicFile(filePath);
// if (!isMusicFile) {
// toast("不是音乐文件");
// console.warn(filePath + " 不是音乐文件");
// return;
// }
// //复制文件到音乐目录
// let res = files.copy(filePath, musicDir + files.getName(filePath));
// if (res) {
// toast("导入成功");
// console.log(filePath + " -> " + musicDir + files.getName(filePath));
// } else {
// console.warn("导入失败");
// toast("导入失败");
// }
fileChooser.chooseFileAndCopyTo(musicDir);
}
function selectTracksInteractive(tracksData, lastSelectedTracksNonEmpty) {
//删除没有音符的音轨
for (let i = tracksData.tracks.length - 1; i >= 0; i--) {
if (tracksData.tracks[i].noteCount == 0) {
tracksData.tracks.splice(i, 1);
}
}
let nonEmptyTrackCount = tracksData.tracks.length;
if (nonEmptyTrackCount === 1) {
dialogs.alert("提示", "只有一条音轨,无需选择");
return [0];
}
if (typeof (lastSelectedTracksNonEmpty) == "undefined" || lastSelectedTracksNonEmpty.length === 0){
lastSelectedTracksNonEmpty = [];
for (let i = 0; i < nonEmptyTrackCount; i++) {
lastSelectedTracksNonEmpty.push(i); //默认选择所有音轨
}
}
let trackInfoStrs = [];
for (let i = 0; i < nonEmptyTrackCount; i++) {
let track = tracksData.tracks[i];
let avgPitch = 0;
for (let j = 0; j < track.notes.length; j++) {
avgPitch += track.notes[j][0];
}
avgPitch /= track.notes.length;
trackInfoStrs.push(track.name + " (" + track.noteCount + "个音符, 平均音高" + avgPitch.toFixed(1) + ")");
}
let selectedTracksNonEmpty = /** @type {Number[]} */ (dialogs.multiChoice("选择音轨", trackInfoStrs, lastSelectedTracksNonEmpty)) ;
if (selectedTracksNonEmpty.length == 0) { //取消选择, 保持原样
selectedTracksNonEmpty = lastSelectedTracksNonEmpty;
}
return selectedTracksNonEmpty;
}
/**
* @param {noteUtils.Note[]} noteData
* @param {number} targetMajorPitchOffset
* @param {number} targetMinorPitchOffset
* @brief 测试配置效果
* @return {{
* "outRangedNoteWeight": number,
* "overFlowedNoteCnt": number,
* "underFlowedNoteCnt": number,
* "roundedNoteCnt": number,
* "totalNoteCnt": number,
* }}
*/
function evalFileConfig(noteData, targetMajorPitchOffset, targetMinorPitchOffset) {
//丢弃音调高的音符的代价要高于丢弃音调低的音符的代价, 因此权重要高
const overFlowedNoteWeight = 5;
const passManager = new PassManager();
let overFlowedNoteCnt = 0;
let underFlowedNoteCnt = 0;
let outRangedNoteWeight = 0;
let roundedNoteCnt = 0;
passManager.reset();
passManager.addPass("NoteToKeyPass", {
majorPitchOffset: targetMajorPitchOffset,
minorPitchOffset: targetMinorPitchOffset,
treatHalfAsCeiling: false,
currentGameProfile: gameProfile,
}, (progress) => { }, (data, statistics, elapsedTime) => {
console.log("生成按键耗时" + elapsedTime / 1000 + "秒");
overFlowedNoteCnt = statistics.overFlowedNoteCnt;
underFlowedNoteCnt = statistics.underFlowedNoteCnt;
outRangedNoteWeight = overFlowedNoteCnt * overFlowedNoteWeight + underFlowedNoteCnt;
roundedNoteCnt = statistics.roundedNoteCnt;
}).run(noteData);
return {
"outRangedNoteWeight": outRangedNoteWeight,
"overFlowedNoteCnt": overFlowedNoteCnt,
"underFlowedNoteCnt": underFlowedNoteCnt,
"roundedNoteCnt": roundedNoteCnt,
"totalNoteCnt": noteData.length,
};
}
/**
* @brief 自动调整文件配置, 包括移调和音轨选择
* @param {string} fileName
* @param {number} trackDisableThreshold 如果一个音轨中超过这个比例的音符被丢弃, 就不选择这个音轨
* @returns
*/
function autoTuneFileConfig(fileName,trackDisableThreshold) {
const betterResultThreshold = 0.05; //如果新的结果比旧的结果好超过这个阈值,就认为新的结果更好
const possibleMajorPitchOffset = [0, -1, 1, -2, 2];
const possibleMinorPitchOffset = [0, 1, -1, 2, -2, 3, -3, 4, -4, 5, 6, 7];
let bestMajorPitchOffset = 0;
let bestMinorPitchOffset = 0;
let bestResult = { "outRangedNoteWeight": 10000000, "roundedNoteCnt": 10000000 };
let bestOverFlowedNoteCnt = 0;
let bestUnderFlowedNoteCnt = 0;
//悬浮窗提示
let dial = dialogs.build({
title: "调整中...",
content: "正在调整音高偏移量,请稍候...",
progress: {
max: possibleMajorPitchOffset.length + possibleMinorPitchOffset.length,
showMinMax: true
},
});
dial.show();
const passManager = new PassManager();
let tracksData = /** @type {MusicFormats.TracksData} */ (passManager.addPass("ParseSourceFilePass").run(musicDir + fileName));
let noteData = new Array();
//合并所有音轨.
for (let i = 0; i < tracksData.trackCount; i++) {
let track = tracksData.tracks[i];
noteData = noteData.concat(track.notes);
}
for (let i = 0; i < possibleMajorPitchOffset.length; i++) {
dial.setProgress(i);
//只考虑超范围的音符
let result = evalFileConfig(noteData, possibleMajorPitchOffset[i], 0);
console.log("Pass " + i + " 结果: " + JSON.stringify(result));
if (bestResult.outRangedNoteWeight - result.outRangedNoteWeight > result.outRangedNoteWeight * betterResultThreshold) {
bestMajorPitchOffset = possibleMajorPitchOffset[i];
bestResult.outRangedNoteWeight = result.outRangedNoteWeight;
}
}
for (let i = 0; i < possibleMinorPitchOffset.length; i++) {
dial.setProgress(possibleMajorPitchOffset.length + i);
//只考虑被四舍五入的音符
let result = evalFileConfig(noteData, bestMajorPitchOffset, possibleMinorPitchOffset[i]);
console.log("Pass " + i + " 结果: " + JSON.stringify(result));
if (bestResult.roundedNoteCnt - result.roundedNoteCnt > result.roundedNoteCnt * betterResultThreshold) {
bestMinorPitchOffset = possibleMinorPitchOffset[i];
bestOverFlowedNoteCnt = result.overFlowedNoteCnt;
bestUnderFlowedNoteCnt = result.underFlowedNoteCnt;
bestResult = result;
}
}
console.info("最佳结果: " + JSON.stringify(bestResult));
console.info("最佳八度偏移: " + bestMajorPitchOffset);
console.info("最佳半音偏移: " + bestMinorPitchOffset);
//禁用无效音符过多的音轨
tracksData = removeEmptyTracks(tracksData);
let selectedTracksNonEmpty = new Array();
if (tracksData.haveMultipleTrack) {
let trackPlayableNoteRatio = new Array();
for (let i = 0; i < tracksData.trackCount; i++) {
let track = tracksData.tracks[i];
let playableNoteCnt = 0;
let result = evalFileConfig(track.notes, bestMajorPitchOffset, bestMinorPitchOffset);
playableNoteCnt = track.notes.length - result.overFlowedNoteCnt - result.underFlowedNoteCnt;
trackPlayableNoteRatio.push([i, playableNoteCnt / track.notes.length]);
}
trackPlayableNoteRatio.sort((a, b) => {
return b[1] - a[1]; //从大到小排序
});
console.log("音轨可用音符比例: " + JSON.stringify(trackPlayableNoteRatio));
selectedTracksNonEmpty = new Array();
selectedTracksNonEmpty.push(trackPlayableNoteRatio[0][0]);
trackPlayableNoteRatio.shift();
for (let i = 0; i < trackPlayableNoteRatio.length; i++) {
let obj = trackPlayableNoteRatio[i];
if (obj[1] > trackDisableThreshold) {
selectedTracksNonEmpty.push(obj[0]);
}
}
console.info("选择的音轨: " + JSON.stringify(selectedTracksNonEmpty));
}
dial.dismiss();
let realBestOutRangedNoteCnt = bestOverFlowedNoteCnt + bestUnderFlowedNoteCnt;
let totalNoteCnt = noteData.length;
/**
* example:
* 最佳结果:
* 超出范围被丢弃的音符数: 123 (+10, -113)(12.34%)
* 被取整的音符数: 456 (56.78%)
* 最佳八度偏移: 0
* 最佳半音偏移: 0
*/
let percentStr1 = (realBestOutRangedNoteCnt / totalNoteCnt * 100).toFixed(2) + "%";
let percentStr2 = (bestResult.roundedNoteCnt / totalNoteCnt * 100).toFixed(2) + "%";
let resultStr = "最佳结果: \n" +
"超出范围被丢弃的音符数: " + realBestOutRangedNoteCnt + " (+" + bestOverFlowedNoteCnt + ", -" + bestUnderFlowedNoteCnt + ")(" + percentStr1 + ")\n" +
"被取整的音符数: " + bestResult.roundedNoteCnt + " (" + percentStr2 + ")\n" +
"最佳八度偏移: " + bestMajorPitchOffset + "\n" +
"最佳半音偏移: " + bestMinorPitchOffset;
if (tracksData.haveMultipleTrack)
resultStr += "\n选择的音轨: " + JSON.stringify(selectedTracksNonEmpty);
dialogs.alert("调整结果", resultStr);
configuration.setFileConfigForTarget("majorPitchOffset", bestMajorPitchOffset, fileName, gameProfile);
configuration.setFileConfigForTarget("minorPitchOffset", bestMinorPitchOffset, fileName, gameProfile);
configuration.setFileConfigForTarget("lastSelectedTracksNonEmpty", selectedTracksNonEmpty, fileName, gameProfile);
toast("自动调整完成");
return 0;
}
function runClickPosSetup() {
let pos1 = getPosInteractive("最上面那行按键中最左侧的按键中心");
let pos2 = getPosInteractive("最下面那行按键中最右侧的按键中心");
console.log("自定义坐标:左上[" + pos1.x + "," + pos1.y + "],右下[" + pos2.x + "," + pos2.y + "]");
gameProfile.setKeyPosition([pos1.x, pos1.y], [pos2.x, pos2.y]);
saveUserGameProfile();
}
/**
* @brief 将一个数值转换到0-1000的另一个区间, 给进度条用
* @param {number} value
* @param {number} min
* @param {number} max
* @returns {number}
*/
function numberMap(value, min, max) {
const newMin = 0;
const newMax = 1000;
if (value < min) value = min;
if (value > max) value = max;
return (value - min) / (max - min) * (newMax - newMin) + newMin;
}
/**
* @brief numberMap的对数版本
* @param {number} value
* @param {number} min
* @param {number} max
* @returns {number}
* @see numberMap
*/
function numberMapLog(value, min, max) {
const newMin = 0;
const newMax = 1000;
if (value < min) value = min;
if (value > max) value = max;
return Math.log(value - min + 1) / Math.log(max - min + 1) * (newMax - newMin) + newMin;
}
/**
* @brief numberMap的反函数
* @param {number} value
* @param {number} min
* @param {number} max
* @returns {number}
* @see numberMap
*/
function numberRevMap(value, min, max) {
const newMin = 0;
const newMax = 1000;
return (value - newMin) / (newMax - newMin) * (max - min) + min;
}
/**
* @brief numberMapLog的反函数
* @param {number} value
* @param {number} min
* @param {number} max
* @returns {number}
* @see numberMapLog
*/
function numberRevMapLog(value, min, max) {
const newMin = 0;
const newMax = 1000;
return min + (Math.exp((value - newMin) / (newMax - newMin) * Math.log(max - min + 1)) - 1);
}
function runFileConfigSetup(fullFileName) {
let fileName = fullFileName;
let rawFileName = musicFormats.getFileNameWithoutExtension(fileName);
let configChanged = false;
const maxClickSpeedHz = 20;
const view = ui.inflate(
<ScrollView margin="0dp" padding="0dp">
<vertical margin="0dp" padding="0dp">
<card cardElevation="5dp" cardCornerRadius="2dp" margin="2dp" contentPadding="2dp">
<vertical>
<text text="速度控制:" textColor="red" />
<horizontal>
{/* 5~1500%, 对数, 默认1->不使用 */}
<text text="变速:" />
<checkbox id="speedMultiplier" />
<text text="default%" id="speedMultiplierValueText" gravity="right|center_vertical" layout_gravity="right|center_vertical" layout_weight="1" />
</horizontal>
<seekbar id="speedMultiplierSeekbar" w="*" max="1000" layout_gravity="center" />
<horizontal w="*">
{/* 1~20hz, 对数 , 默认0->不使用*/}
<text text="限制点击速度(在变速后应用):" />
<checkbox id="limitClickSpeedCheckbox" />
<text text="default次/秒" id="limitClickSpeedValueText" gravity="right|center_vertical" layout_gravity="right|center_vertical" layout_weight="1" />
</horizontal>
<seekbar id="limitClickSpeedSeekbar" w="*" max="1000" layout_gravity="center" />
</vertical>
</card>
<card cardElevation="5dp" cardCornerRadius="2dp" margin="2dp" contentPadding="2dp">
<vertical>
<text text="时长控制(输出):" textColor="red" />
{/* 音符时长输出模式 */}
<horizontal>
<text text="时长输出模式:" />
<radiogroup id="noteDurationOutputMode" orientation="horizontal" padding="0dp" margin="0dp" layout_height="wrap_content">
<radio id="noteDurationOutputMode_none" text="固定值" textSize="12sp" margin="0dp" />
<radio id="noteDurationOutputMode_native" text="真实时长(实验性)" textSize="12sp" margin="0dp" />
{/* <radio id="noteDurationOutputMode_extraLongKey" text="额外长音按钮" textSize="12sp" margin="0dp" /> */}
</radiogroup>
</horizontal>
{/* 默认点击时长 */}
<horizontal w="*">
<text text="默认点击时长: " />
{/* <radiogroup id="defaultClickDurationMode" orientation="horizontal" padding="0dp" margin="0dp" layout_height="wrap_content">
固定的值, 1~500ms, 对数, 默认5ms
<radio id="defaultClickDurationMode_fixed" text="固定值" textSize="12sp" margin="0dp" selected="true" />
音符间隔的比例, 例如0.5代表点击时长为到下一个音符的间隔的一半. 0.05~0.98, 线性, 默认0.5
<radio id="defaultClickDurationMode_intervalRatio" text="音符间隔比例" textSize="12sp" margin="0dp" />
</radiogroup> */}
<text text="defaultms" id="defaultClickDurationValueText" gravity="right|center_vertical" layout_gravity="right|center_vertical" layout_weight="1" />
</horizontal>
<seekbar id="defaultClickDurationSeekbar" w="*" max="1000" layout_gravity="center" />
{/* 最长手势持续时间: 100~30000ms, 对数, 默认8000ms */}
<horizontal w="*">
<text text="最长手势持续时间: " />
<text text="defaultms" id="maxGestureDurationValueText" gravity="right|center_vertical" layout_gravity="right|center_vertical" layout_weight="1" />
</horizontal>
<seekbar id="maxGestureDurationSeekbar" w="*" max="1000" layout_gravity="center" />
{/* 按键间留空时间: 1~600ms, 对数, 默认100ms */}
<horizontal w="*">
<text text="按键间留空时间: " />
<text text="defaultms" id="marginDurationValueText" gravity="right|center_vertical" layout_gravity="right|center_vertical" layout_weight="1" />
</horizontal>
<seekbar id="marginDurationSeekbar" w="*" max="1000" layout_gravity="center" />
</vertical>
</card>
<card cardElevation="5dp" cardCornerRadius="2dp" margin="2dp" contentPadding="2dp">
<vertical>
<text text="音域优化:" textColor="red" />
{/* <ImageView w="*" h="1dp" bg="#a0a0a0" /> */}
<horizontal>
{/* 默认向下取整 */}
<text text="半音处理方法:" layout_gravity="center_vertical" />
<radiogroup id="halfCeilingSetting" orientation="horizontal" padding="0dp" margin="0dp" layout_height="wrap_content">
<radio id="halfCeilingSetting_roundDown" text="向下取整" textSize="12sp" margin="0dp" />
<radio id="halfCeilingSetting_roundUp" text="向上取整" textSize="12sp" margin="0dp" />
</radiogroup>
</horizontal>
<horizontal>
{/* 1~99%, 线性, 默认50% */}
<text text="自动调整: 禁用音轨阈值(越高->越简单):" />
<text text="default%" id="trackDisableThresholdValueText" gravity="right|center_vertical" layout_gravity="right|center_vertical" layout_weight="1" />
</horizontal>
<seekbar id="trackDisableThresholdSeekbar" w="*" max="1000" layout_gravity="center" />
<horizontal>
<button id="autoTuneButton" text="自动优化以下设置(重要!)" />
</horizontal>
<horizontal>
{/* -2~2 */}
<text text="升/降八度:" />
<text text="default" id="majorPitchOffsetValueText" gravity="right|center_vertical" layout_gravity="right|center_vertical" layout_weight="1" />
</horizontal>
<seekbar id="majorPitchOffsetSeekbar" w="*" max="4" layout_gravity="center" />
<horizontal>
{/* -4~7 */}
<text text="升/降半音(移调):" />
<text text="default" id="minorPitchOffsetValueText" gravity="right|center_vertical" layout_gravity="right|center_vertical" layout_weight="1" />
</horizontal>
<seekbar id="minorPitchOffsetSeekbar" w="*" max="11" layout_gravity="center" />
<horizontal>
<text text="音轨选择:" />
<button id="selectTracksButton" text="选择..." padding="0dp" />
</horizontal>
</vertical>
</card>
<card cardElevation="5dp" cardCornerRadius="2dp" margin="2dp" contentPadding="2dp">
<vertical>
<horizontal w="*">
<text text="和弦优化:" textColor="red" />
<checkbox id="chordLimitCheckbox" />
</horizontal>
<horizontal w="*">
<text text="最多同时按键数量: " />
{/* 1-9个, 默认2 */}
<text text="default个" id="maxSimultaneousNoteCountValueText" gravity="right|center_vertical" layout_gravity="right|center_vertical" layout_weight="1" />
</horizontal>
<seekbar id="maxSimultaneousNoteCountSeekbar" w="*" max="1000" layout_gravity="center" />
<horizontal>
{/* 默认向下取整 */}
<text text="按键数量限制方法: " layout_gravity="center_vertical" />
<radiogroup id="noteCountLimitMode" orientation="horizontal" padding="0dp" margin="0dp" layout_height="wrap_content">
<radio id="noteCountLimitMode_delete" text="删除超出的" textSize="12sp" margin="0dp" />
<radio id="noteCountLimitMode_split" text="拆分成多组" textSize="12sp" margin="0dp" />
</radiogroup>
</horizontal>
<horizontal w="*">
<text text="拆分成多组时组间间隔: " />
{/* 5-500ms, 对数, 默认75ms */}
<text text="defaultms" id="noteCountLimitSplitDelayValueText" gravity="right|center_vertical" layout_gravity="right|center_vertical" layout_weight="1" />
</horizontal>
<seekbar id="noteCountLimitSplitDelaySeekbar" w="*" max="1000" layout_gravity="center" />
<horizontal w="*">
<text text="选择方式: " />
<radiogroup id="chordSelectMode" orientation="horizontal" padding="0dp" margin="0dp" layout_height="wrap_content">
<radio id="chordSelectMode_high" text="优先高音" textSize="12sp" margin="0dp" />
<radio id="chordSelectMode_low" text="优先低音" textSize="12sp" margin="0dp" />
<radio id="chordSelectMode_random" text="随机" textSize="12sp" margin="0dp" />
</radiogroup>
</horizontal>
</vertical>
</card>
<card cardElevation="5dp" cardCornerRadius="2dp" margin="2dp" contentPadding="2dp">
<vertical>
<text text="伪装手弹(全局):" textColor="red" />
<horizontal w="*">
{/* 5~150ms, 线性, 默认0->不使用*/}
<text text="音符时间偏差: " />
<checkbox id="noteTimeDeviationCheckbox" />
<text text="defaultms" id="noteTimeDeviationValueText" gravity="right|center_vertical" layout_gravity="right|center_vertical" layout_weight="1" />
</horizontal>
<seekbar id="noteTimeDeviationSeekbar" w="*" max="1000" layout_gravity="center" />
<horizontal w="*">
{/* 0~6mm, 线性, 默认1*/}
<text text="点击位置偏差: " />
<text text="defaultmm" id="clickPositionDeviationValueText" gravity="right|center_vertical" layout_gravity="right|center_vertical" layout_weight="1" />
</horizontal>
<seekbar id="clickPositionDeviationSeekbar" w="*" max="1000" layout_gravity="center" />
</vertical>
</card>
</vertical>
</ScrollView>
);
//回调函数们
view.limitClickSpeedSeekbar.setOnSeekBarChangeListener((seekBar, progress, fromUser) => {
if (progress == undefined) return;
let value = numberRevMapLog(progress, 1, maxClickSpeedHz);
view.limitClickSpeedValueText.setText(value.toFixed(2) + "次/秒");
return true;
});
view.speedMultiplierSeekbar.setOnSeekBarChangeListener((seekBar, progress, fromUser) => {
if (progress == undefined) return;
let value = numberRevMapLog(progress, 0.05, 15);
view.speedMultiplierValueText.setText((value * 100).toFixed(2) + "%");
return true;
});
view.defaultClickDurationSeekbar.setOnSeekBarChangeListener((seekBar, progress, fromUser) => {
if (progress == undefined) return;
let value = numberRevMapLog(progress, 1, 500);
view.defaultClickDurationValueText.setText(value.toFixed(2) + "ms");
return true;
});
view.maxGestureDurationSeekbar.setOnSeekBarChangeListener((seekBar, progress, fromUser) => {
if (progress == undefined) return;
let value = numberRevMapLog(progress, 100, 30000);
view.maxGestureDurationValueText.setText(value.toFixed(2) + "ms");
return true;
});
view.marginDurationSeekbar.setOnSeekBarChangeListener((seekBar, progress, fromUser) => {
if (progress == undefined) return;
let value = numberRevMapLog(progress, 1, 600);
view.marginDurationValueText.setText(value.toFixed(2) + "ms");
return true;
});
view.trackDisableThresholdSeekbar.setOnSeekBarChangeListener((seekBar, progress, fromUser) => {
if (progress == undefined) return;
let value = numberRevMap(progress, 1, 99);
view.trackDisableThresholdValueText.setText(value.toFixed(2) + "%");
return true;
});
view.noteTimeDeviationSeekbar.setOnSeekBarChangeListener((seekBar, progress, fromUser) => {
if (progress == undefined) return;
let value = numberRevMap(progress, 5, 150);
view.noteTimeDeviationValueText.setText(value.toFixed(2) + "ms");
return true;
});
view.clickPositionDeviationSeekbar.setOnSeekBarChangeListener((seekBar, progress, fromUser) => {
if (progress == undefined) return;
let value = numberRevMap(progress, 0, 6);
view.clickPositionDeviationValueText.setText(value.toFixed(2) + "mm");
return true;
});
view.autoTuneButton.click(() => {
let trackDisableThreshold = numberRevMap(view.trackDisableThresholdSeekbar.getProgress(), 1, 99) / 100;
threads.start(function () { //TODO: 重构?
autoTuneFileConfig(fileName, trackDisableThreshold);
ui.run(() => {
let majorPitchOffset = configuration.readFileConfigForTarget("majorPitchOffset", rawFileName, gameProfile, 0);
view.majorPitchOffsetValueText.setText(majorPitchOffset.toFixed(0));
view.majorPitchOffsetSeekbar.setProgress(majorPitchOffset + 2);
let minorPitchOffset = configuration.readFileConfigForTarget("minorPitchOffset", rawFileName, gameProfile, 0);
view.minorPitchOffsetValueText.setText(`${minorPitchOffset.toFixed(0)} (${midiPitch.getTranspositionName(minorPitchOffset)})`);
view.minorPitchOffsetSeekbar.setProgress(minorPitchOffset + 4);
});
});
});
view.majorPitchOffsetSeekbar.setOnSeekBarChangeListener((seekBar, progress, fromUser) => {
if (progress == undefined) return;
let value = progress - 2;
view.majorPitchOffsetValueText.setText(value.toFixed(0));
return true;
});
view.minorPitchOffsetSeekbar.setOnSeekBarChangeListener((seekBar, progress, fromUser) => {
if (progress == undefined) return;
let value = progress - 4;
view.minorPitchOffsetValueText.setText(`${value.toFixed(0)} (${midiPitch.getTranspositionName(value)})`);
return true;
});
view.selectTracksButton.click(() => {
threads.start(function () {
const passManager = new PassManager();
let dialog = dialogs.build({
title: "加载中...",
content: "正在加载数据...",
}).show();
let tracksData = passManager.addPass("ParseSourceFilePass").run(musicDir + fileName);
dialog.dismiss();
let lastSelectedTracksNonEmpty = configuration.readFileConfigForTarget("lastSelectedTracksNonEmpty", rawFileName, gameProfile);
let result = selectTracksInteractive(tracksData, lastSelectedTracksNonEmpty);
configuration.setFileConfigForTarget("lastSelectedTracksNonEmpty", result, rawFileName, gameProfile);
});
});
view.maxSimultaneousNoteCountSeekbar.setOnSeekBarChangeListener((seekBar, progress, fromUser) => {
if (progress == undefined) return;
let value = numberRevMap(progress, 1, 9);
view.maxSimultaneousNoteCountValueText.setText(value.toFixed(0));
return true;
});
view.noteCountLimitSplitDelaySeekbar.setOnSeekBarChangeListener((seekBar, progress, fromUser) => {
if (progress == undefined) return;
let value = numberRevMapLog(progress, 5, 500);
view.noteCountLimitSplitDelayValueText.setText(value.toFixed(0) + "ms");
return true;
});
let finished = false;
dialogs.build({
customView: view,
title: "乐曲配置",
positive: "确定",
negative: "取消"
}).on("show", (dialog) => {
///速度控制
let limitClickSpeedHz = readFileConfig("limitClickSpeedHz", rawFileName, 0);
let speedMultiplier = readFileConfig("speedMultiplier", rawFileName, 1);
view.limitClickSpeedCheckbox.setChecked(limitClickSpeedHz != 0);
view.limitClickSpeedValueText.setText(limitClickSpeedHz.toFixed(2) + "次/秒");
view.limitClickSpeedSeekbar.setProgress(numberMapLog(limitClickSpeedHz, 1, maxClickSpeedHz));
view.speedMultiplier.setChecked(speedMultiplier != 1);
view.speedMultiplierValueText.setText((speedMultiplier * 100).toFixed(2) + "%");
view.speedMultiplierSeekbar.setProgress(numberMapLog(speedMultiplier, 0.05, 15));
//时长控制
let noteDurationOutputMode = configuration.readFileConfigForTarget("noteDurationOutputMode", rawFileName, gameProfile, "none");
switch (noteDurationOutputMode) {
case "none":
view.noteDurationOutputMode_none.setChecked(true);
break;
case "native":
view.noteDurationOutputMode_native.setChecked(true);
break;
}
let defaultClickDuration = readGlobalConfig("defaultClickDuration", 5);
view.defaultClickDurationValueText.setText(defaultClickDuration.toFixed(2) + "ms");
view.defaultClickDurationSeekbar.setProgress(numberMapLog(defaultClickDuration, 1, 500));
let maxGestureDuration = readGlobalConfig("maxGestureDuration", 8000);
view.maxGestureDurationValueText.setText(maxGestureDuration.toFixed(2) + "ms");
view.maxGestureDurationSeekbar.setProgress(numberMapLog(maxGestureDuration, 100, 30000));
let marginDuration = readGlobalConfig("marginDuration", 100);
view.marginDurationValueText.setText(marginDuration.toFixed(2) + "ms");