Files
bettergi-scripts-list/repo/js/YuanQinAssistant/main.js
himno 58d543fb8d 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
2025-11-15 01:22:57 +08:00

379 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(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();
})();