mirror of
https://github.com/babalae/bettergi-scripts-list.git
synced 2026-03-20 04:10:00 +08:00
JS脚本:YuanQinAssistant (#2347)
* Add score for '银月之庭(蓝花)C大调' * Create tutorial file(empty now) Add placeholder for video tutorial link * Delete repo/js/YuanQinAssistant/assets/toos directory * Create tutorial placeholder file Add placeholder for tutorial video link * 添加转谱工具 * 发布 原琴助手 v1.0.0 * Fixed some incorrect descriptions
This commit is contained in:
55
repo/js/YuanQinAssistant/README.md
Normal file
55
repo/js/YuanQinAssistant/README.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# YuanQinAssistant
|
||||
|
||||
原琴助手BGI版是用于游戏中自动弹奏的脚本,使用gs2格式的曲谱,并提供方便的midi转谱工具。
|
||||
|
||||
## 内容清单
|
||||
- [快速开始](#快速开始)
|
||||
- [曲谱格式说明](#曲谱格式说明)
|
||||
- [MIDI转谱](#midi转谱)
|
||||
|
||||
## 快速开始
|
||||
1. 在“全自动-调度器”中新建配置组,并添加此脚本。
|
||||
2. 右键-修改JS脚本自定义配置,选择曲目后,点击运行即可在游戏中自动演奏,请先确保角色处于演奏状态。
|
||||
3. 曲谱文件保存在 `assets/score/` 文件夹中,如果此文件夹中的内容发生变动,脚本会自动更新曲谱列表并退出,再次运行后方可选择曲谱进行演奏。
|
||||
|
||||
## 曲谱格式说明
|
||||
脚本使用指令式的文本曲谱,每一行都代表一个指令,以“!xx/something”的形式出现。不符合此格式的行会被忽略,因此可以利用此特性在曲谱中插入注释文本。
|
||||
|
||||
1. 文件头
|
||||
```text
|
||||
!v/2
|
||||
```
|
||||
- 曲谱第一行固定为此内容。
|
||||
|
||||
2. 说明性指令
|
||||
```text
|
||||
!ti/标题
|
||||
!au/发布人
|
||||
!cp/作曲人
|
||||
!ar/制谙人
|
||||
```
|
||||
- 说明曲谱文件的版权信息,非必须。这些指令不存在时会使用缺省值。
|
||||
|
||||
3. 演奏参数指令
|
||||
```text
|
||||
!gi/乐器编号
|
||||
!di/四分音符的tick数
|
||||
```
|
||||
- 乐器编号用于说明曲谱适用的乐器,从0开始,非必须,缺省值为0,表示风物之诗琴。此指令在脚本中并无实际作用,是为手机端适配不同乐器而预留的。
|
||||
- di指令用于指定MIDI规范中的division值,非必需,缺省值为480。改变此值相当于改变BPM,但需要和轨道事件中的变速事件结合,才能最终确定每个音符的时长。
|
||||
|
||||
4. 轨道指令
|
||||
```text
|
||||
!tr/事件序列
|
||||
```
|
||||
- 可以同时存在多个轨道指令,本脚本通过多个轨道事件的同步,来实现和弦功能。
|
||||
- 事件序列由一串形如 **1000n60** 的文本组成,并以竖线 **`|`** 分隔。其中1000表示此事件之前经过的时间,以tick数计算,具体可查询MIDI文件规范,此处不作讲解。n是事件类型,60是此事件的参数。
|
||||
- 目前支持三种事件,分别为:
|
||||
- n 按下音符,参数表示音符的音高(60即为C4)
|
||||
- u 松开音符,参数含义同 n
|
||||
- s 变速,参数表示接下来的microtempo值,单位是微秒
|
||||
|
||||
## MIDI转谱
|
||||
您完全可以通过文本编辑器来手动编辑一个gs2格式的曲谱文件,但并不建议这么做。本质上,gs2格式是因为BGI不支持二进制文件的读取,而不得不做的一个折中方案。使用现有的专业工具制作出mid格式文件,再使用我提供的转谱工具生成对应的gs2文件(工具放置于 `assets/tool/` 文件夹下),是更直观且更安全的做法。
|
||||
|
||||
您需要注意的是,为了简化处理逻辑,转谱工具对mid文件的内容作了一些限制,比如不能使用异步多轨的格式,必须是C大调,导出midi文件时不要包含RPN和NRPN相关指令等等,更详细的会在之后用视频教程进行演示,普通用户完全不了解这些内容也不影响使用。
|
||||
9
repo/js/YuanQinAssistant/assets/score/银月之庭(蓝花)C大调.gs2
Normal file
9
repo/js/YuanQinAssistant/assets/score/银月之庭(蓝花)C大调.gs2
Normal file
@@ -0,0 +1,9 @@
|
||||
!v/2
|
||||
!gi/0
|
||||
!ti/银月之庭(蓝花)C大调
|
||||
!au/天空光芒
|
||||
!cp/HoyoMix
|
||||
!ar/Gideon Chamberlain
|
||||
!di/480
|
||||
!tr/0s731707|0n60|43n64|44n71|151u64|0u71|1u60|1n57|239u57|1n59|239u59|1n60|239u60|1n67|959u67|1n60|43n64|44n71|151u64|0u71|1u60|1n57|239u57|1n59|239u59|1n60|239u60|1n67|239u67|1n59|719u59|1n60|43n64|44n71|151u64|0u71|1u60|1n57|239u57|1n59|239u59|1n60|239u60|1n67|959u67|1n60|43n64|44n71|151u64|0u71|1u60|1n57|239u57|1n59|239u59|1n60|119u60|1n60|119u60|1n62|959u62|1n76|479u76|1n81|479u81|1n81|719u81|1n79|239u79|1n81|479u81|1n79|239u79|1n78|239u78|1n74|959u74|1n76|479u76|1n72|479u72|1n74|239u74|1n76|239u76|1n79|479u79|1n76|179u76|1n77|59u77|1n76|119u76|1n74|119u74|1n76|959u76|481n76|479u76|1n81|479u81|1n81|239u81|481n79|239u79|1n81|479u81|1n79|239u79|1n78|239u78|1n74|959u74|1n76|479u76|1n72|479u72|1n74|239u74|1n72|239u72|1n71|239u71|1n67|239u67|1n72|119u72|1n71|119u71|1n69|479u69|241n72|239u72|241n74|239u74|241n76|359u76|1n76|39u76|1n77|39u77|1n76|39u76|1n74|239u74|1n72|239u72|1n74|359u74|1n74|39u74|1n76|39u76|1n74|39u74|1n72|239u72|1n71|239u71|1n72|239u72|1n71|239u71|1n69|479u69|1n72|239u72|1n71|239u71|1n72|239u72|1n74|239u74|1n76|359u76|1n76|39u76|1n77|39u77|1n76|39u76|1n74|239u74|1n72|239u72|1n74|239u74|241n72|239u72|1n71|239u71|1n76|959u76|1n72|479u72|1n74|479u74|1n76|359u76|1n76|39u76|1n77|39u77|1n76|39u76|1n74|239u74|1n72|239u72|1n74|239u74|241n72|239u72|1n71|239u71|1n72|239u72|1n71|239u71|1n69|479u69|1n69|239u69|1n71|239u71|1n72|239u72|1n71|239u71|1n72|119u72|1n71|119u71|1n69|479u69|1n76|239u76|1n74|239u74|1n72|239u72|1n71|239u71|1n67|239u67|1n72|119u72|1n71|119u71|1n69|959u69|1681n64|0n69|959u64|0u69|1n64|0n76|959u64|0u76|1n76|0n88|719u76|0u88|1n74|0n86|119u74|0u86|1n72|0n84|119u72|0u84|1n71|0n83|959u71|0u83|1n67|0n79|479u67|0u79|1n69|0n81|479u69|0u81|1n76|479u76|1n81|479u81|1n81|719u81|1n79|239u79|1n81|479u81|1n79|239u79|1n78|239u78|1n74|959u74|1n76|479u76|1n72|479u72|1n74|239u74|1n76|239u76|1n79|479u79|1n76|179u76|1n77|59u77|1n76|119u76|1n74|119u74|1n76|959u76|481n76|479u76|1n81|479u81|1n81|239u81|481n79|239u79|1n81|479u81|1n79|239u79|1n78|239u78|1n74|959u74|1n76|479u76|1n72|479u72|1n74|239u74|1n72|239u72|1n71|239u71|1n67|239u67|1n72|119u72|1n71|119u71|1n69|479u69|241n72|239u72|241n74|239u74|241n76|359u76|1n76|39u76|1n77|39u77|1n76|39u76|1n74|239u74|1n72|239u72|1n74|359u74|1n74|39u74|1n76|39u76|1n74|39u74|1n72|239u72|1n71|239u71|1n72|239u72|1n71|239u71|1n69|479u69|1n72|239u72|1n71|239u71|1n72|239u72|1n74|239u74|1n76|359u76|1n76|39u76|1n77|39u77|1n76|39u76|1n74|239u74|1n72|239u72|1n74|239u74|241n72|239u72|1n71|239u71|1n76|959u76|1n72|479u72|1n74|479u74|1n76|359u76|1n76|39u76|1n77|39u77|1n76|39u76|1n74|239u74|1n72|239u72|1n74|239u74|241n72|239u72|1n71|239u71|1n72|239u72|1n71|239u71|1n69|479u69|1n69|239u69|1n71|239u71|1n72|239u72|1n71|239u71|1s731707|0n72|119u72|1n71|119u71|1s755311|0n69|240s780488|239u69|1s807401|0n76|239u76|1s836237|0n74|239u74|1s867208|0n72|239u72|1s900563|0n71|239u71|1s936585|0n67|239u67|1s975610|0n69|1919u69|961n69|239u69|1n71|239u71|1n72|239u72|1n71|239u71|1n72|119u72|1n71|119u71|1n69|479u69|1n76|239u76|1n74|239u74|1n72|239u72|1n71|239u71|1n67|239u67|1n69|1920s1463414|1919s731707|0u69
|
||||
!tr/0n52|0n57|239u52|0u57|1681n52|0n57|239u52|0u57|1681n52|0n57|239u52|0u57|1681n57|239u57|1681n57|42n64|1876u64|1u57|1n55|42n64|1876u64|1u55|1n60|42n64|1876u64|1u60|1n57|42n64|1876u64|1u57|1n57|43n64|915u64|1u57|1n55|43n62|915u62|1u55|1n53|43n60|915u60|1u53|1n52|43n60|915u60|1u52|1n57|43n64|915u64|1u57|1n55|43n62|915u62|1u55|1n57|43n60|195u60|1u57|1n52|239u52|1n57|239u57|1n59|239u59|1n60|479u60|1n59|479u59|1n57|43n64|915u64|1u57|1n55|43n62|915u62|1u55|1n57|43n60|195u60|1u57|1n52|239u52|1n57|239u57|1n59|239u59|1n60|479u60|1n59|479u59|1n57|43n64|915u64|1u57|1n55|43n62|915u62|1u55|1n57|43n60|675u60|1u57|1n52|239u52|1n57|0n60|479u57|0u60|1n55|0n62|239u55|0u62|1n55|239u55|1n57|43n64|915u64|1u57|1n55|43n62|435u62|1u55|1n55|479u55|1n57|43n60|195u60|1u57|1n52|239u52|1n57|239u57|1n59|239u59|1n60|959u60|1n55|43n64|195u64|1u55|1n52|239u52|1n57|239u57|1n59|239u59|1n55|43n62|915u62|1u55|1n57|43n60|195u60|1u57|1n52|239u52|1n57|239u57|1n59|239u59|1n64|239u64|1n66|239u66|1n67|479u67|1n45|239u45|1n52|239u52|1n57|239u57|1n59|1199u59|1n45|239u45|1n52|239u52|1n57|239u57|1n59|239u59|1n64|959u64|1n45|239u45|1n52|239u52|1n57|239u57|1n59|239u59|1n62|959u62|1n57|42n64|1876u64|1u57|1n55|42n64|1876u64|1u55|1n60|43n64|1395u64|1u60|1n59|43n64|435u64|1u59|1n57|43n64|915u64|1u57|1n64|239u64|241n57|239u57|241n57|43n64|915u64|1u57|1n55|43n62|915u62|1u55|1n53|43n60|915u60|1u53|1n52|43n60|915u60|1u52|1n57|43n64|915u64|1u57|1n55|43n62|915u62|1u55|1n57|43n60|195u60|1u57|1n52|239u52|1n57|239u57|1n59|239u59|1n60|479u60|1n59|479u59|1n57|43n64|1395u64|1u57|1n55|43n62|435u62|1u55|1n57|43n60|195u60|1u57|1n52|239u52|1n57|239u57|1n59|239u59|1n60|479u60|1n59|479u59|1n57|43n64|915u64|1u57|1n55|43n62|915u62|1u55|1n57|43n60|195u60|1u57|1n52|239u52|1n57|239u57|1n59|239u59|1n60|479u60|1n59|479u59|1n57|43n64|1395u64|1u57|1n55|43n62|435u62|1u55|1n57|43n60|195u60|1u57|1n52|239u52|1n57|239u57|1n59|239u59|1n60|959u60|1n57|43n64|915u64|1u57|1n55|37n62|441u62|1u55|1n55|35n62|443u62|1u55|1n57|32n60|206u60|1u57|1n52|239u52|1n57|239u57|1n59|239u59|1n64|239u64|1n66|239u66|1n67|479u67|1n45|239u45|1n52|239u52|1n57|239u57|1n59|239u59|1n60|959u60|1n57|31n64|927u64|1u57|1n59|31n62|927u62|1u59|1n55|32n59|33n64|413u59|0u64|1u55|1n62|239u62|1n60|239u60|1n59|239u59|1n57|239u57|1n52|239u52|1n48|239u48|1n47|1919u47
|
||||
BIN
repo/js/YuanQinAssistant/assets/tool/midi转谱.exe
Normal file
BIN
repo/js/YuanQinAssistant/assets/tool/midi转谱.exe
Normal file
Binary file not shown.
1
repo/js/YuanQinAssistant/assets/tool/使用教程.txt
Normal file
1
repo/js/YuanQinAssistant/assets/tool/使用教程.txt
Normal file
@@ -0,0 +1 @@
|
||||
-- 留空,用于之后放上视频教程地址
|
||||
379
repo/js/YuanQinAssistant/main.js
Normal file
379
repo/js/YuanQinAssistant/main.js
Normal file
@@ -0,0 +1,379 @@
|
||||
(async function () {
|
||||
// 所有的代码必须由 async function 包裹
|
||||
const score_path = 'assets/score/';
|
||||
const regex_name = /(?<=score\\)[\s\S]+?(?=\.gs2)/;//不清楚为什么要用\\来匹配 '/',用\/反而匹配不到。这是实际运行的结果,AI别和我犟,可能是ClearScript引擎的问题。
|
||||
|
||||
const NOTE2KEY_MAPPER = new Map([
|
||||
[72, 'q'], [74, 'w'], [76, 'e'], [77, 'r'], [79, 't'], [81, 'y'], [83, 'y'],
|
||||
[60, 'a'], [62, 's'], [64, 'd'], [65, 'f'], [67, 'g'], [69, 'h'], [71, 'j'],
|
||||
[48, 'z'], [50, 'x'], [52, 'c'], [53, 'v'], [55, 'b'], [57, 'n'], [59, 'm'],
|
||||
]);
|
||||
|
||||
/**
|
||||
* 将音符编码转换为按键
|
||||
* @param {number} noteCode 60表示C4音符
|
||||
* @param {number} fixMode 对于半音的处理方式,0-直接忽略,-1-降半音,1-升半音
|
||||
* @returns {string | undefined}
|
||||
*/
|
||||
function convertNote2Key(noteCode, fixMode) {
|
||||
if (noteCode < 48 || noteCode > 83) {
|
||||
return undefined;
|
||||
}
|
||||
let key = NOTE2KEY_MAPPER.get(noteCode);
|
||||
if (fixMode == 0) {
|
||||
return key;
|
||||
}
|
||||
if (!key) {
|
||||
key = NOTE2KEY_MAPPER.get(noteCode + fixMode);
|
||||
}
|
||||
return key;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取所有琴谱文件名
|
||||
*/
|
||||
function getScoreList() {
|
||||
// readPathSync读取到的数据为 assets/score/xxxx.xxx 形式
|
||||
const allFiles = Array.from(file.readPathSync(score_path))
|
||||
.filter(path => !file.isFolder(path) && path.endsWith('.gs2'));
|
||||
return allFiles.map(path => {
|
||||
const match = path.match(regex_name);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
return match[0];
|
||||
})
|
||||
.filter(name => !!name);
|
||||
};
|
||||
|
||||
/**
|
||||
* 补全完整路径(相对路径)
|
||||
* @param {string} filename
|
||||
* @returns
|
||||
*/
|
||||
function buildFullpath(filename) {
|
||||
return score_path + filename + '.gs2';
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 轨道事件
|
||||
* @typedef {Object} TrackEvent
|
||||
* @property {number} dt delta time
|
||||
* @property {string} command 指令:n-按下音符,u-松开音符,s-改变速度(MicroTempo)
|
||||
* @property {number} value 指令的值,对与n和u,表示音符的编码,对于s,表示一个四分音符对应的微秒数(MicroTempo)
|
||||
*/
|
||||
/**
|
||||
* 曲谱信息
|
||||
* @typedef {Object} ScoreInfo
|
||||
* @property {number} instrument 乐器编号
|
||||
* @property {number} division 每个四分音符的tick数
|
||||
* @property {string} title 标题
|
||||
* @property {string} author 曲谱文件的作者
|
||||
* @property {string} composer 作曲者
|
||||
* @property {string} arranger 制谱者(通常指扒谱的人)
|
||||
* @property {Array<TrackEvent>[]} tracks 音符轨道
|
||||
*/
|
||||
|
||||
/**
|
||||
* 从指定文件中加载琴谱
|
||||
* @param {string} filename 琴谱文件名
|
||||
* @returns {ScoreInfo} 返回一个ScoreInfo对象
|
||||
*/
|
||||
function loadScoreInfo(filename) {
|
||||
const filepath = buildFullpath(filename);
|
||||
try {
|
||||
/** @type {string} */
|
||||
const content = file.readTextSync(filepath);
|
||||
const lines = content.split(/\r?\n/);
|
||||
if (lines[0] !== '!v/2') {
|
||||
log.error('错误的文件头!');
|
||||
return null;
|
||||
}
|
||||
/** @type {ScoreInfo} */
|
||||
const info = {
|
||||
instrument: 0,
|
||||
division: 480,
|
||||
title: filename,
|
||||
author: '未知',
|
||||
arranger: '未知',
|
||||
composer: '未知',
|
||||
tracks: [],
|
||||
};
|
||||
const lineReg = /^!(\w+?)\/(.+)$/;
|
||||
lines.forEach(line => {
|
||||
const match = line.match(lineReg);
|
||||
if (match) {
|
||||
switch(match[1]) {
|
||||
case 'gi': // 乐器编号
|
||||
info.instrument = parseInt(match[2]);
|
||||
break;
|
||||
case 'di': // division
|
||||
info.division = parseInt(match[2]);
|
||||
break;
|
||||
case 'ti': // title
|
||||
info.title = match[2];
|
||||
break;
|
||||
case 'au': // author 曲谱发布人
|
||||
info.author = match[2];
|
||||
break;
|
||||
case 'cp': // composer 作曲人
|
||||
info.composer = match[2];
|
||||
break;
|
||||
case 'ar': // arranger 制谱人
|
||||
info.arranger = match[2];
|
||||
break;
|
||||
case 'tr': // 事件轨道
|
||||
info.tracks.push(parseTrackEvents(match[2]));
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
return info;
|
||||
} catch (error) {
|
||||
log.error(`加载曲谱文件 ${filename} 时发生错误`, error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 从文本中解析出轨道事件
|
||||
* @param {string} text 事件的字符串
|
||||
* @returns {TrackEvent[]}
|
||||
*/
|
||||
function parseTrackEvents(text) {
|
||||
const regex = /^(\d+)([nus])(\d+)$/;
|
||||
return text.split('|')
|
||||
.map(t => {
|
||||
const match = t.match(regex);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
const dt = parseInt(match[1]);
|
||||
if (isNaN(dt) || dt < 0 || dt > Number.MAX_SAFE_INTEGER) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
dt,
|
||||
command: match[2],
|
||||
value: parseInt(match[3]),
|
||||
};
|
||||
|
||||
})
|
||||
.filter(v => !!v);
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查本地的文件列表和设置中的选项是否一致
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function checkScoreSheet() {
|
||||
try {
|
||||
// 获取本地琴谱文件名列表
|
||||
const localMusicList = getScoreList();
|
||||
|
||||
// 读取配置文件
|
||||
/** @type {Array} */
|
||||
const settingsList = JSON.parse(file.readTextSync('settings.json'));
|
||||
/** @type {string[]} */
|
||||
let configMusicList = undefined; // 配置文件中的gs2文件名列表
|
||||
let selectorIndex = -1; // 音乐选择器在配置列表中的序号
|
||||
for(let i = 0; i < settingsList.length; i++) {
|
||||
if(settingsList[i].name === 'music_selector') {
|
||||
selectorIndex = i;
|
||||
configMusicList = settingsList[i].options;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 核对两个列表是否相同
|
||||
let totallySame = true;
|
||||
if (localMusicList.length !== configMusicList.length) {
|
||||
totallySame = false;
|
||||
}
|
||||
else {
|
||||
for (let i = 0; i < localMusicList.length; i++) {
|
||||
if (localMusicList[i] !== configMusicList[i]) {
|
||||
totallySame = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果不相同,则以本地列表为准,更新到配置文件中去
|
||||
if (!totallySame) {
|
||||
const updatedSettings = [...settingsList];
|
||||
updatedSettings[selectorIndex].options = localMusicList;
|
||||
file.writeTextSync("settings.json", JSON.stringify(updatedSettings, null, 4));
|
||||
log.warn("检测到曲谱文件不一致, 已自动修改settings(以本地曲谱文件为基准)...");
|
||||
log.warn("JS脚本配置已更新, 请重新运行脚本!");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
log.error('检查曲谱文件时发生错误:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
//---------------------------------------------------------
|
||||
|
||||
/** 事件源 */
|
||||
const eventSource = {
|
||||
indices: [], // 各轨道当前索引
|
||||
deltas: [], // 各轨道剩余delta time
|
||||
division: 480, // 一个四分音符的tick数
|
||||
microtempo: 50_0000, // 一个四分音符的微秒数
|
||||
/**
|
||||
* 初始化事件源
|
||||
* @param {ScoreInfo} scoreInfo
|
||||
*/
|
||||
init(scoreInfo) {
|
||||
this.division = scoreInfo.division;
|
||||
this.microtempo = 50_0000;
|
||||
this.indices = new Array(scoreInfo.tracks.length).fill(0);
|
||||
this.deltas = scoreInfo.tracks.map(l => l[0].dt);
|
||||
},
|
||||
/**
|
||||
* 开始依次将轨道事件发射到接收器
|
||||
* @param {*} receiver 接收器
|
||||
* @param {ScoreInfo} scoreInfo
|
||||
*/
|
||||
async produce(receiver, scoreInfo) {
|
||||
while(true) {
|
||||
// 选择delta time最小的轨道
|
||||
let targetTrackIndex = -1;
|
||||
let minDelta = Number.MAX_SAFE_INTEGER;
|
||||
for (let i = 0; i < this.deltas.length; i++) {
|
||||
if (this.indices[i] >= scoreInfo.tracks[i].length) {
|
||||
continue;
|
||||
}
|
||||
const dt = this.deltas[i];
|
||||
if (dt < minDelta) {
|
||||
targetTrackIndex = i;
|
||||
minDelta = dt;
|
||||
}
|
||||
}
|
||||
|
||||
// 所有轨道都处理完
|
||||
if (targetTrackIndex < 0) {
|
||||
break;
|
||||
}
|
||||
// 选定事件
|
||||
let targetEvent = scoreInfo.tracks[targetTrackIndex][this.indices[targetTrackIndex]];
|
||||
|
||||
// 将选定轨道的索引往后移动一位,并更新其对应的delta time
|
||||
this.indices[targetTrackIndex] += 1;
|
||||
if (this.indices[targetTrackIndex] < scoreInfo.tracks[targetTrackIndex].length) {
|
||||
this.deltas[targetTrackIndex] = scoreInfo.tracks[targetTrackIndex][this.indices[targetTrackIndex]].dt;
|
||||
}
|
||||
else {
|
||||
this.deltas[targetTrackIndex] = Number.MAX_SAFE_INTEGER;
|
||||
}
|
||||
// 其他轨道对应的delta time相应减少
|
||||
for (let i = 0; i < this.deltas.length; i++) {
|
||||
const dt = this.deltas[i];
|
||||
if (i !== targetTrackIndex) {
|
||||
this.deltas[i] = Math.max(dt - minDelta, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// 等待
|
||||
if (minDelta > 0) {
|
||||
let waitMilliseconds = ((this.microtempo * minDelta / this.division) / 1000) | 0;
|
||||
await sleep(waitMilliseconds);
|
||||
}
|
||||
|
||||
// 发送事件到接收器
|
||||
receiver.receive(targetEvent, this);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 事件接收器
|
||||
*/
|
||||
const eventReceiver = {
|
||||
keyStates: new Map(),
|
||||
fixMode: 0,
|
||||
init(fixMode) {
|
||||
this.fixMode = fixMode;
|
||||
this.keyStates.clear();
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {TrackEvent} event
|
||||
* @param {*} source
|
||||
*/
|
||||
receive(event, source) {
|
||||
// log.info(event.command + ':' + event.value);
|
||||
let k;
|
||||
switch (event.command) {
|
||||
case 's': // 改变microtempo
|
||||
source.microtempo = event.value;
|
||||
break;
|
||||
case 'n': // 按下音符
|
||||
k = convertNote2Key(event.value, this.fixMode);
|
||||
if (k && (!this.keyStates.get(k))) {
|
||||
this.keyStates.set(k, true);
|
||||
keyDown(k);
|
||||
}
|
||||
break;
|
||||
case 'u': // 松开音符
|
||||
k = convertNote2Key(event.value, this.fixMode);
|
||||
if (k && this.keyStates.get(k)) {
|
||||
this.keyStates.set(k, false);
|
||||
keyUp(k);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
function getFixMode() {
|
||||
const modeDesc = settings.fix_mode;
|
||||
if (modeDesc == '降半音') {
|
||||
return -1;
|
||||
}
|
||||
else if (modeDesc == '升半音') {
|
||||
return 1;
|
||||
}
|
||||
else {
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
function getInstrumentName(instrumentCode) {
|
||||
const list = ["风物之诗琴", "老旧的诗琴", "镜花之琴", "盛世豪鼓", "绮庭之鼓", "晚风圆号", "余音", "悠可琴", "跃律琴"];
|
||||
return list[instrumentCode] || "未知乐器";
|
||||
}
|
||||
|
||||
|
||||
//----------------------------------------------------------
|
||||
async function main() {
|
||||
if (!checkScoreSheet()) return;
|
||||
const scoreFilename = settings.music_selector;
|
||||
if (!scoreFilename) {
|
||||
log.warn('未选择曲谱,请在js配置中选择后再次运行脚本');
|
||||
return;
|
||||
}
|
||||
|
||||
const scoreInfo = loadScoreInfo(scoreFilename);
|
||||
const instrumentName = getInstrumentName(scoreInfo.instrument);
|
||||
log.info('当前演奏:' + scoreInfo.title);
|
||||
log.info(`作曲人:${scoreInfo.composer},制谱人:${scoreInfo.arranger}`);
|
||||
log.info(`推荐乐器:${instrumentName},发布人:${scoreInfo.author}`);
|
||||
|
||||
eventSource.init(scoreInfo);
|
||||
eventReceiver.init(getFixMode());
|
||||
|
||||
// 开始演奏
|
||||
await eventSource.produce(eventReceiver, scoreInfo);
|
||||
};
|
||||
|
||||
await main();
|
||||
})();
|
||||
15
repo/js/YuanQinAssistant/manifest.json
Normal file
15
repo/js/YuanQinAssistant/manifest.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"manifest_version": 1,
|
||||
"name": "原琴助手",
|
||||
"version": "1.0.0",
|
||||
"bgi_version": "0.43.1",
|
||||
"description": "BetterGI版本的原琴助手,使用gs2格式的琴谱",
|
||||
"authors": [
|
||||
{
|
||||
"name": "himno",
|
||||
"link": "https://github.com/himno"
|
||||
}
|
||||
],
|
||||
"settings_ui": "settings.json",
|
||||
"main": "main.js"
|
||||
}
|
||||
20
repo/js/YuanQinAssistant/settings.json
Normal file
20
repo/js/YuanQinAssistant/settings.json
Normal file
@@ -0,0 +1,20 @@
|
||||
[
|
||||
{
|
||||
"name": "music_selector",
|
||||
"type": "select",
|
||||
"label": "选择一首曲子",
|
||||
"options": [
|
||||
"银月之庭(蓝花)C大调"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "fix_mode",
|
||||
"type": "select",
|
||||
"label": "半音修正",
|
||||
"options": [
|
||||
"忽略",
|
||||
"降半音",
|
||||
"升半音"
|
||||
]
|
||||
}
|
||||
]
|
||||
Reference in New Issue
Block a user