diff --git a/repo/js/YuanQinAssistant/README.md b/repo/js/YuanQinAssistant/README.md new file mode 100644 index 000000000..b5438633f --- /dev/null +++ b/repo/js/YuanQinAssistant/README.md @@ -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相关指令等等,更详细的会在之后用视频教程进行演示,普通用户完全不了解这些内容也不影响使用。 \ No newline at end of file diff --git a/repo/js/YuanQinAssistant/assets/score/银月之庭(蓝花)C大调.gs2 b/repo/js/YuanQinAssistant/assets/score/银月之庭(蓝花)C大调.gs2 new file mode 100644 index 000000000..dd32901d0 --- /dev/null +++ b/repo/js/YuanQinAssistant/assets/score/银月之庭(蓝花)C大调.gs2 @@ -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 diff --git a/repo/js/YuanQinAssistant/assets/tool/midi转谱.exe b/repo/js/YuanQinAssistant/assets/tool/midi转谱.exe new file mode 100644 index 000000000..4ddf490e8 Binary files /dev/null and b/repo/js/YuanQinAssistant/assets/tool/midi转谱.exe differ diff --git a/repo/js/YuanQinAssistant/assets/tool/使用教程.txt b/repo/js/YuanQinAssistant/assets/tool/使用教程.txt new file mode 100644 index 000000000..0f0ec30fd --- /dev/null +++ b/repo/js/YuanQinAssistant/assets/tool/使用教程.txt @@ -0,0 +1 @@ +-- 留空,用于之后放上视频教程地址 diff --git a/repo/js/YuanQinAssistant/main.js b/repo/js/YuanQinAssistant/main.js new file mode 100644 index 000000000..edcb9c965 --- /dev/null +++ b/repo/js/YuanQinAssistant/main.js @@ -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[]} 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(); +})(); \ No newline at end of file diff --git a/repo/js/YuanQinAssistant/manifest.json b/repo/js/YuanQinAssistant/manifest.json new file mode 100644 index 000000000..84001681d --- /dev/null +++ b/repo/js/YuanQinAssistant/manifest.json @@ -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" +} \ No newline at end of file diff --git a/repo/js/YuanQinAssistant/settings.json b/repo/js/YuanQinAssistant/settings.json new file mode 100644 index 000000000..d5ece6fe7 --- /dev/null +++ b/repo/js/YuanQinAssistant/settings.json @@ -0,0 +1,20 @@ +[ + { + "name": "music_selector", + "type": "select", + "label": "选择一首曲子", + "options": [ + "银月之庭(蓝花)C大调" + ] + }, + { + "name": "fix_mode", + "type": "select", + "label": "半音修正", + "options": [ + "忽略", + "降半音", + "升半音" + ] + } +] \ No newline at end of file