From 509f39decf50889767d52b54b04ac3f3460d9cb1 Mon Sep 17 00:00:00 2001 From: NuperAki <55879805+TheHeartFickle@users.noreply.github.com> Date: Thu, 11 Sep 2025 08:39:38 +0800 Subject: [PATCH] Work done (#1860) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AutoYuanQin新版适配,优化了定时播放和配置更新,简化了代码 --- repo/js/AutoYuanQin/README.md | 381 ++++---- .../score_file/0010.卡农(MIDI转谱).json | 1 + .../assets/score_file/0011.圆号卡农.json | 11 + .../assets/tutorial_file/MIDI翻谱器.html | 1 + repo/js/AutoYuanQin/last_settings.json | 1 + repo/js/AutoYuanQin/main.js | 860 ++++++++++++------ repo/js/AutoYuanQin/manifest.json | 6 +- repo/js/AutoYuanQin/settings.json | 57 +- 8 files changed, 816 insertions(+), 502 deletions(-) create mode 100644 repo/js/AutoYuanQin/assets/score_file/0011.圆号卡农.json create mode 100644 repo/js/AutoYuanQin/last_settings.json diff --git a/repo/js/AutoYuanQin/README.md b/repo/js/AutoYuanQin/README.md index e94cb9792..2554173a3 100644 --- a/repo/js/AutoYuanQin/README.md +++ b/repo/js/AutoYuanQin/README.md @@ -1,49 +1,68 @@ -# 曲谱 JSON 文件说明 -* **注意** - - 制谱优先使用AutoYuanQin\assets\tutorial_file目录下的制谱软件(五线谱制谱器.html),有任何疑问请来看这个使用说明 -此文档供曲谱制作人阅读,本文档详细说明了一个标准格式的曲谱.json文件格式,包括各个字段的解释以及曲谱内容的格式要求。 +# 原琴 - AutoYuanQin +- [播放演奏脚本](#播放演奏脚本) + - [上传至仓库](#上传至仓库) + - [本地播放](#本地播放) +- [MIDI翻谱器](#MIDI翻谱器) +- [五线谱翻谱器](#五线谱翻谱器) +- [曲谱制作解答](#曲谱制作解答) +- [曲谱文件位置](#曲谱文件位置) +- [文件结构](#文件结构) + - [字段说明](#字段说明) + - [文本格式](#文本格式) + - [代码美化](#代码美化) +- [Notes 解析规则](#解析规则) +- [音符格式](#音符格式) +- [更新日志](#更新日志) -重要:即使制作了曲谱的JSON文件,放到了正确的路径下,在调度器的JS脚本配置里也不会出现你制作的曲谱(上传方法如下) +## 播放演奏脚本 +1. 在全自动 - 调度器中 新建配置组/选择已有的组 将*AutoYuanQin*添加至配置组中 +1. 右键原琴 - 修改脚本自定义配置 按照说明填写 +1. 点击运行开始播放音乐 +- tips: + - `AutoYuanQin/assets/tutorial_file`文件夹下的制谱软件(`五线谱制谱器.html`)为早期制谱用软件 + - 现在有更自动化的`AutoYuanQin/tools/MIDI翻谱器.html`提供自动的MIDI文件转*AutoYuanQin*格式的乐谱 + - `AutoYuanQin/assets/tutorial_file`目录下有文档供曲谱制作人阅读 + - 本文档下文会详细说明一个标准格式的曲谱.json文件格式, 包括各个字段的解释以及曲谱内容的格式要求 + - 即使将你制作的曲谱的JSON文件置于正确的路径也无法在调度器的JS脚本配置里出现你制作的曲谱, 需要上传至仓库 -## 上传方法 -1.上传到BetterGI脚本仓库的repo\js\AutoLyre\assets\score_file路径下,根据已存在的曲谱在你的文件名前添加序号(例如 10.曲名.json),完成后请联系BetterGI v7群主更新JS脚本 +### 上传至仓库 +1. 上传到BetterGI脚本仓库的[路径](https://github.com/babalae/bettergi-scripts-list/tree/main/repo/js/AutoYuanQin/assets)下, 根据已存在的曲谱在你的文件名前添加序号`例如 10.曲名.json`, 完成后请联系BetterGI v7群主更新JS脚本 +1. 联系BetterGI v7(1029539994)群主帮你更新到仓库 +1. 发送邮件到*hijiwos@hotmail.com*并说明, 你的谱子将会在一段时间内更新到仓库 -2.联系BetterGI v7(1029539994)群主帮你更新到仓库 +### 本地播放 +1. 如果你暂时不将乐谱上传至仓库, 那么你可以在"单曲名称"中将你置于`AutoYuanQin/assets`下的乐谱名称输入, 此自定义配置的优先级高于单曲选择 -3.发送邮件到hijiwos@hotmail.com并说明,你的谱子将会在一段时间内更新到仓库 +## MIDI翻谱器 +1. 翻谱器位于`AutoYuanQin/tools/MIDI翻谱器.html`,请使用浏览器打开 +1. 点击选择文件, 将你想要转换的MIDI文件放入, 网页将自动开启转换 +1. 完成转换后点击"导出Json"后可将文件转移至`AutoYuanQin/assets`目录下使用[本地播放](#本地播放) +1. **请不要在不了解文件结构的情况下手动修改JSON文件中的信息** -## MIDI翻谱器使用方法 -**MIDI翻谱器路径: AutoYuanQin\assets\tutorial_file\MIDI翻谱器.html** -**声明:本翻谱器生成的曲谱文件为标准格式(区别于五线谱制谱器,生成的是MIDI版本的JSON标准格式)** - -使用浏览器打开```MIDI翻谱器.html```即可,注意**千万不要手动修改翻谱器生成的JSON文件中的author** - -## 曲谱制作器使用方法 -**制谱器路径: AutoYuanQin\assets\tutorial_file\五线谱制谱器.html(请确保 五线谱注解.png与制谱器位于同一目录下)** -**声明:本制谱器生成的曲谱文件为标准格式** - -* 使用步骤如下(顺序) - * 确定音域(共有三种音域可选[左中右共三个],每个音域为一对红蓝大写字符[21个]) - * 选择音符:点击左上角图片中的对应大写字母或@, 点击多个音符实现和弦 - * 完善音符:页面底部两行选择音符的具体类型,选好后点击按钮```确定(完善音符)``` - * 分节:确保当前页面的音符都已完善,点击按钮```分节``` - * 换行:确保当前页面的音符都已完善,点击按钮```换行``` +## 五线谱翻谱器 +1. 五线谱翻谱器位于`AutoYuanQin/assets/tutorial_file/五线谱制谱器.html`,请使用浏览器打开 +1. 请确保你有一定的识谱能力, 否则本文建议使用[MIDI翻谱器](#MIDI翻谱器) +1. 使用步骤如下 + 1. 确定音域(共有三种音域可选[左中右共三个], 每个音域为一对红蓝大写字符[21个]) + 1. 选择音符:点击左上角图片中的对应`大写字母`或`@`, 点击多个音符实现和弦 + 1. 完善音符:页面底部两行选择音符的具体类型, 选好后点击按钮```确定(完善音符)``` + 1. 分节:确保当前页面的音符都已完善, 点击按钮```分节``` + 1. 换行:确保当前页面的音符都已完善, 点击按钮```换行``` - * **删除音符:** 如果您**写错了音符**,请**手动**删除**整个音符**并确保右上角代码框的**末尾**是 ```]、|、或一行的开头``` - * 音符删除示例:```A[16]B[32]``` 比如您想写 ```B[16]``` ,一不小心写成了三十二分音符,请删除整个音符~~B[32]~~,删除后的示例```A[16]```,然后再次进行音符 ```B```的写入 - * 写入曲谱信息(曲名、录谱人必填、bpm、拍号) - * 导出曲谱 - 点击按钮```导出乐谱JSON```,曲谱文件名请确认是```曲名.json``` - * 读取乐谱 - 如果您写了一半,打算下次在写,可以使用导出曲谱功能保存曲谱,下次要写的时候点击按钮```读取乐谱JSON```,选择上次导出的文件即可 + 1. **删除音符:** 如果您**写错了音符**, 请**手动**删除**整个音符**并确保右上角代码框的**末尾**是 ```]、|、或一行的开头``` + * 音符删除示例:```A[16]B[32]``` 比如您想写 ```B[16]``` , 一不小心写成了三十二分音符, 请删除整个音符~~B[32]~~, 删除后的示例```A[16]```, 然后再次进行音符 ```B```的写入 + 1. 写入曲谱信息(曲名、录谱人必填、bpm、拍号) + 1. 导出曲谱 + 点击按钮```导出乐谱JSON```, 曲谱文件名请确认是```曲名.json``` + 1. 读取乐谱 + 如果您写了一半, 打算下次在写, 可以使用导出曲谱功能保存曲谱, 下次要写的时候点击按钮```读取乐谱JSON```, 选择上次导出的文件即可 -## 曲谱制作问题 -`\assets\tutorial_file\五线谱注解.png` 包含了五线谱(高音区和低音区)对应的3组键盘键位(相邻的红蓝大写字母为一组,每组音域为三个八度) - -有不懂的地方请在 `\assets\tutorial_file\example.json` 内找,这个谱子内包含了该脚本的五线谱相关的所有功能 +## 曲谱制作解答 +1. `/assets/tutorial_file/五线谱注解.png` 包含了五线谱(高音区和低音区)对应的3组键盘键位(相邻的红蓝大写字母为一组, 每组音域为三个八度) +1. 有不懂的地方请在 `/assets/tutorial_file/example.json` 内找, 这个谱子内包含了该脚本的五线谱相关的所有功能 ## 曲谱文件位置 -所有的曲谱文件应放置于 `AutoLyre\assets\score_file` 路径下,并在文件名前添加正确的序号 +1. 所有的曲谱文件应放置于 `AutoYuanQin/assets` 路径下 ## 文件结构 一个标准的曲谱.json文件的基本结构如下: @@ -51,7 +70,9 @@ { "name": "", "author": "", + "instrument": "", "description": "", + "type":"", "bpm": "", "time_signature": "", "composer": "", @@ -60,135 +81,34 @@ } ``` -**注意**:以上代码中仅 : 右侧的**双引号内**的部分可以更改,具体的曲谱格式请参考 `\assets\tutorial_file` 路径下的 `example.json` +**注意**:以上代码中仅 : 右侧的**双引号内**的部分可以更改, 具体的曲谱格式请参考 `/assets/tutorial_file` 路径下的 `example.json` -## 字段说明 -**name**: 曲谱名,必填。 +### 字段说明 +`name`: **必要键值** 曲谱名 -**author**: 录谱人,制作这个.json曲谱的作者名。 +`author`: **可选键值** 录谱人, 制作这个.json曲谱的作者名 -**description**: 描述,可以随意填写关于该曲谱的附加信息 +`instrument`: **可选键值** 此键值对用于建议用户的乐器使用 -**bpm**: 曲谱的BPM(Beats Per Minute),必填。 +`description`: **可选键值** 可以随意填写关于该曲谱的描述信息 -**time_signature**: 拍号,必填,例如 3/4 代表 以四分音符为一拍每小节三拍(被设为一拍的音符仅支持2的幂)。 +`type`: **必要键值** 决定曲谱的解析方式, 合法的值有`yuanqin`(默认值, [五线谱翻谱器](#五线谱翻谱器)) `midi`([MIDI翻谱器](#MIDI翻谱器)) `keyboard`(你在网上看到的琴谱格式与此相似) -**composer**: 曲师,选填。 +`bpm`: **必要键值** 曲谱的BPM (Beats Per Minute) -**arranger**: 谱师,选填。 +`time_signature`: `yuanqin`的**必要键值** 拍号, 例如 3/4 代表 以四分音符为一拍每小节三拍(被设为一拍的音符仅支持2的幂) -**notes**: 曲谱内容,必填,具体格式请参考以下解析规则。 +`composer`: **可选键值** 曲师 -## Notes 解析规则(重要) -notes 字段中包含的是乐谱内容。音符**必须**使用**大写字母**,乐谱内容使用字符串表示,小节之间用 | 隔开。单个小节的解析规则如下: +`arranger`: **可选键值** 谱师 -### A[4] -表示按下A键,A键视作四分音符。 -
- -

四分音符示例

-
+`notes`: **必要键值** 曲谱内容,具体格式可参考以下解析规则 -### F[16-#]D[16-#]S[16-#] -表示**装饰音·倚音** -
- -

装饰音·倚音示例

-
- -以上每个装饰音的时值固定为拍号中的标准时值(3/4的标准时值为四分音符的时值)的1/16,也就是说以上示例中的**16没有意义,但是必须要写** - -### Z[4-8.3]C[4-8.3]B[4-8.$] -表示一个**三连音**(六连音用法与此相似,仅需将3改成6,**其它类型的连音**也请使用3或6(即使是5连音)) -另外,连音内支持和弦 -
- -

三连音示例

-
- -* Z[4-8.3] - - 4表示该三连音的总时值相当于四分音符,8表示当前音符在乐谱上显示的时值相当于八分音符的时值,3表示这是一个三连音的音符 - -* C[4-8.3] - - 同上 - -* B[4-8.$] - - $表示这是当前连音的最后一个音符 - -### D[4-16.3]G[4-16.3]H[4-16.3]W[4-16.3]R[4-16.$] -表示一个**五连音**,同理也可以是**N连音** -
- -

五连音示例

-
- -* D[4-16.3] - - 4表示该连音的总时值相当于四分音符,16表示当前音符在乐谱上显示的时值相当于十六分音符的时值,3表示这个音符是一个连音的一部分 - -* R[4-16.$] - - $表示这是当前连音的最后一个音符 -### (BG)[4-4.3]\(VF\)[4-8.$] -表示一个**三连音连音线**(与三连音用法相同,但是三连音连音线允许连线内出现不同类型的音符) -
- -

三连音连音线示例

-
- -* (BG)[4-4.3] - - 第一个4表示整个三连音的总时值为一个四分音符,第二个4表示当前音符在乐谱上显示的时值相当于四分音符的时值,3表示这是一个三连音的音符 - -* (VF)[4-8.$] - - 4表示整个三连音的总时值为一个四分音符,8表示这是一个八分音符,$表示这是当前连音的最后一个音符 - -### @[2-8.3]\(AF\)[2-16.3]N[2-16.3]\(AF\)[2-16.3]N[2-16.3]\(AF\)[2-16.3]N[2-16.3]\(AF\)[2-16.3]N[2-16.3]\(AF\)[2-16.3]N[2-16.$] -表示一个**六连音连音线**(乐谱上表示为一个六连音连音线内包含1个八分休止符和10个十六分音符,与三连音相同,六连音的.后面的数字也是3) -
- -

六连音连音线示例

-
- -* @[2-8.3] - - 2表示该六连音的总时值相当于一个二分音符,8表示当前音符在乐谱上显示的时值相当于八分音符的时值,6表示这是一个六连音 - -* N[2-16.$] - - 16表示当前音符在乐谱上显示的时值相当于十六分音符的时值,$表示这是当前连音的最后一个音符 - -### @[4] -表示一个**休止符** -
- -

四分休止符示例

-
- - 中括号内表明这是几分休止符,例如这里表示四分休止符。 - -### (SH)[4-*] -表示一个**附点四分音符** -
- -

附点四分音符示例

-
- - 表示按下S和H键(和弦),这个和弦视作附点四分音符。 - -## 代码美化 -曲谱JSON文件的"notes"的值视作一个字符串,在这个字符串内仅可以使用**换行符**美化代码,通过这种方法可以使用记事本等软件从.json文件中获取带有换行的曲谱代码(**notes内的换行符不会被读取执行**) - -### 格式 - 每一小节的末尾加|\n - - 每一行的末尾加|\n\n - - 曲谱的末尾无需加|和\n +### 文本格式 +1. 一个完整的曲谱.json文件示例如下(供示例, 仅包含几个小节): +1. 每一小节的末尾加|/n +1. 每一行的末尾加|/n/n +1. 曲谱的末尾无需加|和/n 例如: ```json @@ -200,49 +120,154 @@ notes 字段中包含的是乐谱内容。音符**必须**使用**大写字母** "time_signature": "4/4", "composer": "曲师B", "arranger": "谱师C", - "notes": "A[4](ASD)[8]Y[8-#]F[8-#](DFG)[8]R[4-*]T[8]|\n@[4](DFG)[8](CVB)[8]D[4]A[4]|\n\nA[4](ASD)[8]Y[8-#]F[8-#](DFG)[8]R[4]T[4]|\n@[4](DFG)[8](CVB)[8]D[4]A[4]" + "notes": "A[4](ASD)[8]Y[8-#]F[8-#](DFG)[8]R[4-*]T[8]|/n@[4](DFG)[8](CVB)[8]D[4]A[4]|/n/nA[4](ASD)[8]Y[8-#]F[8-#](DFG)[8]R[4]T[4]|/n@[4](DFG)[8](CVB)[8]D[4]A[4]" } ``` +### 代码美化 +曲谱JSON文件的"notes"的值视作一个字符串, 在这个字符串内仅可以使用**换行符**美化代码, 通过这种方法可以使用记事本等软件从.json文件中获取带有换行的曲谱代码(**notes内的换行符不会被读取执行**) -## 附: -中括号 [](- 前表示音符类型,- 后用于区分特殊音符) + +## Notes 解析规则 +notes 字段中包含的是乐谱内容音符**必须**使用**大写字母**, 乐谱内容使用字符串表示, 小节之间用 | 隔开单个小节的解析规则如下: + +### `A[4]` +表示按下A键, A键视作四分音符 +
+ +

四分音符示例

+
+ +### `F[16-#]D[16-#]S[16-#]` +表示**装饰音·倚音** +
+ +

装饰音·倚音示例

+
+ +以上每个装饰音的时值固定为拍号中的标准时值(3/4的标准时值为四分音符的时值)的1/16, 也就是说以上示例中的**16没有意义, 但是必须要写** + +### `Z[4-8.3]C[4-8.3]B[4-8.$]` +表示一个**三连音**(六连音用法与此相似, 仅需将3改成6, **其它类型的连音**也请使用3或6(即使是5连音)) +另外, 连音内支持和弦 +
+ +

三连音示例

+
+ +* `Z[4-8.3] ` + + 4表示该三连音的总时值相当于四分音符, 8表示当前音符在乐谱上显示的时值相当于八分音符的时值, 3表示这是一个三连音的音符 + +* `C[4-8.3] ` + + 同上 + +* `B[4-8.$]` + + $表示这是当前连音的最后一个音符 + +### `D[4-16.3]G[4-16.3]H[4-16.3]W[4-16.3]R[4-16.$]` +表示一个**五连音**, 同理也可以是**N连音** +
+ +

五连音示例

+
+ +* `D[4-16.3]` + + 4表示该连音的总时值相当于四分音符, 16表示当前音符在乐谱上显示的时值相当于十六分音符的时值, 3表示这个音符是一个连音的一部分 + +* `R[4-16.$]` + + $表示这是当前连音的最后一个音符 +### `(BG)[4-4.3]/(VF/)[4-8.$]` +表示一个**三连音连音线**(与三连音用法相同, 但是三连音连音线允许连线内出现不同类型的音符) +
+ +

三连音连音线示例

+
+ +* (BG)[4-4.3] + + 第一个4表示整个三连音的总时值为一个四分音符, 第二个4表示当前音符在乐谱上显示的时值相当于四分音符的时值, 3表示这是一个三连音的音符 + +* (VF)[4-8.$] + + 4表示整个三连音的总时值为一个四分音符, 8表示这是一个八分音符, $表示这是当前连音的最后一个音符 + +### `@[2-8.3]/(AF/)[2-16.3]N[2-16.3]/(AF/)[2-16.3]N[2-16.3]/(AF/)[2-16.3]N[2-16.3]/(AF/)[2-16.3]N[2-16.3]/(AF/)[2-16.3]N[2-16.$]` +表示一个**六连音连音线**(乐谱上表示为一个六连音连音线内包含1个八分休止符和10个十六分音符, 与三连音相同, 六连音的.后面的数字也是3) +
+ +

六连音连音线示例

+
+ +* `@[2-8.3]` + + 2表示该六连音的总时值相当于一个二分音符, 8表示当前音符在乐谱上显示的时值相当于八分音符的时值, 6表示这是一个六连音 + +* `N[2-16.$] ` + + 16表示当前音符在乐谱上显示的时值相当于十六分音符的时值, $表示这是当前连音的最后一个音符 + +### `@[4]` +表示一个**休止符** +
+ +

四分休止符示例

+
+ + 中括号内表明这是几分休止符, 例如这里表示四分休止符 + +### `(SH)[4-*]` +表示一个**附点四分音符** +
+ +

附点四分音符示例

+
+ + 表示按下S和H键(和弦), 这个和弦视作附点四分音符 + + + + +## 音符格式 +中括号 [](- 前表示音符类型, - 后用于区分特殊音符) * [4] -表示四分音符。 +表示四分音符 * [16] -表示十六分音符。 +表示十六分音符 * [-#] -表示装饰音。 +表示装饰音 * [-n.3] -表示连音(使用时必须保证连音的最后一个音的标记为.$)。 +表示连音(使用时必须保证连音的最后一个音的标记为.$) * [-n.$] 表示当前连音的结束 -例如:[16-#] 表示十六分音符的装饰音,A[4-8.3]S[4-8.3]D[4-8.$] 表示一个总时值为4分音符的三连音。 +例如:[16-#] 表示十六分音符的装饰音, A[4-8.3]S[4-8.3]D[4-8.$] 表示一个总时值为4分音符的三连音 -## 示例 -一个完整的曲谱.json文件示例如下(供示例,仅包含几个小节): -文件名: `示例曲谱.json` +## 更新日志 +由于更新日志于3.0开始记录, 往期更新内容应该都在git的记录中, 不过我懒得翻了 -```json -{ - "name": "示例曲谱", - "author": "录谱人", - "bpm": "120", - "description": "曲谱信息", - "time_signature": "4/4", - "composer": "曲师", - "arranger": "谱师", - "notes": "N[8-#]A[8-#](VS)[1]|\n(NF)[2-*](AG)[4]|\n(SH)[2-*](SH)[8](AG)[8]|\n(FW)[4](VF)[4](BG)[4](NH)[4]|\n\nB[8]N[8]X[16]Z[16]X[16]Z[16]B[8]N[8]X[16]Z[16]X[16]Z[16]|\nB[8]N[8]X[16]Z[16]X[16]Z[16]V[8]C[8]X[8]Z[8]" -} -``` \ No newline at end of file +- ver 3.1.1 +1. 新增了解析基于字符串的按键序列的函数 +1. 新增了执行上述函数序列化按键信息的函数 +1. 初步完成了以上函数与原项目代码的适配 +1. 制谱软件从assets中转移至脚本根路径的./tools文件夹下 +1. 五线谱制谱软件的文件夹名称改为StaffMaker +1. MIDI翻谱器从五线谱制谱器路径中移出 +1. 修改manifest.name为"原琴-音乐转换·自动演奏" +1. 修改settings.json, 使其更易被理解 +1. 增加作者 +1. 修改了README.md \ No newline at end of file diff --git a/repo/js/AutoYuanQin/assets/score_file/0010.卡农(MIDI转谱).json b/repo/js/AutoYuanQin/assets/score_file/0010.卡农(MIDI转谱).json index 9d15456b8..864e955d2 100644 --- a/repo/js/AutoYuanQin/assets/score_file/0010.卡农(MIDI转谱).json +++ b/repo/js/AutoYuanQin/assets/score_file/0010.卡农(MIDI转谱).json @@ -2,6 +2,7 @@ "name": "示例曲谱", "author": "MidiTrans", "bpm": "120", + "type": "midi", "description": "曲谱信息", "time_signature": "4/4", "composer": "曲师", diff --git a/repo/js/AutoYuanQin/assets/score_file/0011.圆号卡农.json b/repo/js/AutoYuanQin/assets/score_file/0011.圆号卡农.json new file mode 100644 index 000000000..e79ffca14 --- /dev/null +++ b/repo/js/AutoYuanQin/assets/score_file/0011.圆号卡农.json @@ -0,0 +1,11 @@ +{ + "name": "圆号D大调卡农", + "author": "半江残秋", + "type": "keyboard", + "instrument": "晚风圆号", + "arranger": "原琴玩家伊蕾娜", + "composer": "帕赫贝尔", + "description": "BV1km411C7EJ", + "bpm": 300, + "notes": "(EQ)---/----/(WG)---/----/(QH)---/----/(JD)---/----/(HF)---/----/(GD)---/----/(HF)---/----/(JG)---/---->(GE)---/----/(GW)---/----/(DQ)---/----/(DJ)---/----/(AH)---/----/(AG)---/---->(AH)---/----/(SJ)---/----/(EA)-G-/Q-E-/(WJ)-S-/G-J-/(QH)-D-/H-Q-/J-D-/G-J-/(HF)-A-/F-H-/(GA)-D->D-A-/(HF)-A-/F-H-/G-S-/F---/(QA)-(JD)-/(QG)-(DA)-/G-S-/(JG)-J-/(QH)-D-/(EH)-A-/(TD)-(ED)-/(TG)-(YJ)-/(RF)-(EA)-/(WF)-(RH)-/(EA)-(WD)-/(QG)-(JA)-/(HF)-(FA)-/(QF)-H-/(JG)-(GS)-/(QG)-J-/(QA)-(JD)-/(QG)-(DA)-/G-S-/(JG)-J-/(QH)-D-/(EH)-(QA)-/(TD)-(ED)-/(TG)-(YJ)-/(RF)-(EA)-/(WF)-(RH)-/(EA)-(WD)-/(QG)-(JA)-/(HF)-(GA)-/F-H-/(QG)-SG>(JG)-(WJ)G/(EA)-DG>(EG)W(QA)W>(WJ)-SE/(RG)E(WJ)E/(QH)-DQ>(QH)-(JA)Q/(JD)-(GD)D>D-G-/(HF)-A-/(JF)-(QH)-/(GA)-D->D-(GA)-/(HF)-A-/FHQ->(QG)-SJ>(JG)Q(WJ)G/(EA)-DG>(EG)W(QA)W>(WG)-SE/(RG)E(WJ)E/(QH)-DQ>(QH)-(JA)Q/(JD)-(GD)D>D-G-/(HF)-A-/(JF)-(QH)-/(GA)-D->D-(GA)-/(HF)-A-/FH(QH)->(QG)-SJ>(JG)Q(WJ)G/(TA)-(EG)R/(TQ)-ER/(TG)J(HS)J/QWER/(EH)-(QD)W/(EA)-DF/(GD)H(GD)F/GQJQ/(HF)-(QA)J/H-GF/(GA)FDF/(GA)HJQ/(HF)-(QA)J/(QH)-JQ>(JG)H(JS)Q/WERT>(TA)-(EG)R/(TA)-ER/(TG)J(HS)J/QWER/(EH)-(QD)W/(EA)-DF>(GD)H(GD)F/(GG)QJQ/(HF)-(QA)J/H-GF/(GA)FDF/(GA)HJQ/(HF)-(QA)J/(QH)-JQ>(JG)H(JS)Q/(WJ)ERT/(EQ)-(QG)W/(EQ)-WQ/(WG)J(QS)W/(EJ)WQJ/(QH)-(HD)J/(QA)-AS/DFDS/DQJQ/(HF)-(QA)J/H-GF>(GA)FDF/(GA)HJQ/(HF)-(QA)J/(QH)-JH>(JG)Q(WS)Q/JQHJ/(QA)-G-/Q-E-/G-S-/G-J-/H-D-/H-Q-/D-D-/G-J-/F-A-/F-H-/A-G-/Q-E-/F-A-/F-H-/G-S-/G-J-/A-(ED)R/(TG)-(EQ)-/G-(WS)E/(RG)-(WJ)-/H-(QD)W/(EH)-Q-/D-(ED)W/(QG)-J-/F-(HA)J/(QF)-H-/A-(GD)H/(QG)-(GA)-/F-(HA)J/(QF)-JH/GG(HS)J/(WG)-J-/A-(ED)R/(TG)-(EA)-/G-(WS)E/(RG)-(WJ)-/HH(QD)W/(EH)-Q-/D-(TD)R/(EG)-(TJ)-/(YF)-(YA)T/(RF)-(YH)-/(TA)-(TD)R/(EG)-(TQ)-/(YF)T(RA)Y/(TF)R(YH)T/(UG)Y(TS)-/(JG)Q(WJ)G/(EA)-D-/(EG)WQW>(WG)-SE>(EG)R(WJ)E/(WH)-DQ>(QH)-JQ/DG(JD)Q/(EG)T(UJ)Q/(UF)Y(TA)R/(TF)R(EH)W/(EA)W(QD)J/(QG)J(HA)G/(HF)G(FA)G/(HF)FHQ/(JG)H(GS)Q>(QG)G(WJ)G/(EA)-D-/(EG)-(RA)-/(TG)-(YS)-/(TG)-(RJ)-/(EH)-D-/(QH)-(WA)-/(ED)-(RD)-/(EG)-(WJ)-/(QF)-A-/(HF)J(QH)Q>(QA)QDQ>(QG)QJQ/(HF)-A-/FH(QH)W/(QG)JSJ>(JG)Q(WJ)G/(EA)-D-/(EG)-(RQ)-/(TG)-(YS)-/(TG)-(RJ)-/(EH)-D-/(QH)-W-/(ED)-(RD)-/(EG)-(WJ)-/(QF)-A-/(HF)J(QH)Q>(QA)QDQ>(QG)Q(JA)Q/(HF)-A-/FH(QH)W/(QG)JSJ>(JG)Q(WJ)G/(EA)-G-/A-D-/(WJ)-S-/G-J-/(QH)-D-/H-Q-/(JD)-D-/G-J-/(HF)-A-/F-H-/(GA)-D-/G-A-/(HF)-A-/F-H-/(JG)-S-/G-J-/(EA)-G-/A-D-/(WJ)-S-/G-J-/(QH)-D-/H-Q-/(JD)-D-/G-J-/(HF)-A-/F-H-/(GA)-D-/G-A-/(HF)-A-/F-H-/(JG)-S-/G-J-/(DQAG)---" +} \ No newline at end of file diff --git a/repo/js/AutoYuanQin/assets/tutorial_file/MIDI翻谱器.html b/repo/js/AutoYuanQin/assets/tutorial_file/MIDI翻谱器.html index d2b3c6955..0d0d43002 100644 --- a/repo/js/AutoYuanQin/assets/tutorial_file/MIDI翻谱器.html +++ b/repo/js/AutoYuanQin/assets/tutorial_file/MIDI翻谱器.html @@ -194,6 +194,7 @@ function parseMidi(arrayBuffer) { bpm: Math.round(60000000 / tempo).toString(), description: "曲谱信息", time_signature: "4/4", + type:"midi", composer: "曲师", arranger: "谱师", notes: mappedNotes diff --git a/repo/js/AutoYuanQin/last_settings.json b/repo/js/AutoYuanQin/last_settings.json new file mode 100644 index 000000000..a6e141486 --- /dev/null +++ b/repo/js/AutoYuanQin/last_settings.json @@ -0,0 +1 @@ +[object Object] \ No newline at end of file diff --git a/repo/js/AutoYuanQin/main.js b/repo/js/AutoYuanQin/main.js index 8923f90a8..a5ea9b93c 100644 --- a/repo/js/AutoYuanQin/main.js +++ b/repo/js/AutoYuanQin/main.js @@ -1,21 +1,209 @@ (async function () { // 待解决问题: 连音总时值如果为3个四分音符无法表示 + const base_path = "assets/score_file/"; + const regex_name = /(?<=score_file\\)[\s\S]*?(?=.json)/; + const PlayType = { + SingleMusicOnce: 0, // 单曲单次执行 + SingleMusicRepeat: 1, // 单曲循环执行 + QueueMusicOnce: 2, // 队列单次执行 + QueueMusicRepeat: 3, // 队列循环执行 + }; + /** + * -------- 工具函数 -------- + */ + /** + * + * @returns {Array} 本地曲谱文件列表 + */ + const musicList = () => { + const scoreFiles = Array.from(file.readPathSync(base_path)).filter(path => !file.isFolder(path) && path.endsWith(".json")); + const localMusicList = scoreFiles.map(path => path.match(regex_name)[0]); + return localMusicList; + } + /** + * + * 根据乐曲文件名生成乐曲文件路径 + * + * @param music_name 乐曲文件名 + * @returns {string} 乐曲文件路径 + */ + function pathJoin(music_name) { + return base_path + music_name + ".json"; + } - // // 乐曲名(带序号) - // const music_list = [ - // "0001.小星星", - // "0002.小星星变奏曲", - // "0003.Unknown Mother Goose [アンノウン・マザーグース]", - // "0004.铃芽之旅[Suzume]", - // "0005.Flower Dance", - // "0006.起风了", - // "0007.千本樱 (Eric Chen)", - // "0008.春よ、来い(春天,来吧)", - // "0009.One Last Kiss", - // "0010.卡农(MIDI转谱)" - // ] - const base_path = "assets/score_file/" - const regex_name = /(?<=score_file\\)[\s\S]*?(?=.json)/ + /** + * 获取JS脚本配置 + * + * @returns {Object} 包含解析后JS脚本配置的对象,具有以下属性: + * @property {Number} startTime - 目标时间的时间戳 + * @property {Number} playType - 播放模式,使用PlayType枚举 + * @property {Array[String]} musicQueue - 乐曲队列,包含乐曲文件名的数组 + * @property {Number} queueInterval - 乐曲队列间隔时间,单位为秒 + * @property {Number} repeatTimes - 循环执行次数 + * @property {Number} repeatInterval - 循环间隔时间,单位为秒 + * + */ + function get_settings() { + const Settings = { + startTime: 0, + playType: undefined, + musicQueue: [], + queueInterval: 0, + repeatTimes: 1, + repeatInterval: 0 + } + + /** + * @param {String} timeString + * @returns {Number} 目标时间运行当天的时间戳 + * @example + * console.log(calTargetTimeStamp('14:30:00')) // at 2025/9/10 + * -> 1757485800000 (2025/9/10 14:30:00)的时间戳 + */ + const calTargetTimeStamp = (timeString) => { + const [hours, minutes, seconds] = timeString.replace(/[^0-9:]/g, "").split(':').map(Number); + + const now = new Date(); + const year = now.getFullYear(); + const month = now.getMonth(); + const day = now.getDate(); + + const localDate = new Date(year, month, day, hours, minutes, seconds); + return localDate.getTime(); + } + try { + // 读取开始时间 + let music_start = typeof (settings.music_start) === 'undefined' ? "00:00:00" : settings.music_start; + Settings.startTime = calTargetTimeStamp(music_start); + // 读取播放模式 + let type_select = typeof (settings.type_select) === 'undefined' ? "单曲单次执行" : settings.type_select; + switch (type_select) { + case "单曲单次执行": + Settings.playType = PlayType.SingleMusicOnce; + break; + case "单曲循环": + Settings.playType = PlayType.SingleMusicRepeat; + break; + case "队列单次执行": + Settings.playType = PlayType.QueueMusicOnce; + break; + case "队列循环": + Settings.playType = PlayType.QueueMusicRepeat; + break; + default: + Settings.playType = PlayType.SingleMusicOnce; + break; + } + + // 读取队列间隔时间 + Settings.queueInterval = (typeof (settings.music_interval) === 'undefined') ? (0) : parseInt(settings.music_interval, 10); + // 读取循环次数 + Settings.repeatTimes = (typeof (settings.music_repeat) === 'undefined') ? (1) : parseInt(settings.music_repeat, 10); + // 读取循环间隔时间 + Settings.repeatInterval = (typeof (settings.repeat_interval) === 'undefined') ? (0) : parseInt(settings.repeat_interval, 10); + // 读取乐曲队列 Array[musicName] + if (Settings.playType === PlayType.SingleMusicOnce || Settings.playType === PlayType.singleRepeat) { + Settings.musicQueue.push((typeof (settings.music_selector) === 'undefined') ? (undefined) : (settings.music_selector)); + } + else { + let musicIndex = (typeof (settings.music_queue) === 'undefined') ? (undefined) : (settings.music_queue); + musicIndex = Array.from(new Set(musicIndex.split(' ').filter(item => item !== ""))); // 去重 + musicList().forEach(music => { + if (music.includes(musicIndex[0])) { + Settings.musicQueue.push(music); + musicIndex.shift(); + } + }); + } + return Settings; + + } catch (error) { + log.error(`读取JS脚本配置时出错:${error}`); + } + } + + /** + * + * 读取并解析一个乐谱文件 + * + * @param music_name {string} 乐曲文件名 + * @returns {Promise<{}|null>} + * @property {string} name 乐曲名称 + * @property {string} author 作者 + * @property {string} instrument 建议乐器 + * @property {string} description 乐曲描述 + * @property {string} type 乐曲类型 + * @property {number} bpm BPM + * @property {string} time_signature 拍号 + * @property {string} composer 作曲者 + * @property {string} arranger 编曲者 + * @property {Object[][]} notes 乐谱内容 + */ + function getMusicInfo(music_name) { + const MusicInfo = { + name: undefined, // 乐曲名称 + author: undefined, // 作者 + instrument: undefined, // 乐器 + description: undefined, // 乐曲描述 + type: undefined, // 乐曲类型 + bpm: undefined, // BPM + time_signature: undefined, // 拍号 + composer: undefined, // 作曲者 + arranger: undefined, // 编曲者 + notes: undefined, // 乐谱内容 + } + + let music_path = pathJoin(music_name); + let file_text = ""; // 存储乐曲文件内容 + // 读取并检查文件 + try { + file_text = file.readTextSync(music_path); + } catch (error) { + log.error(`文件无法读取:${music_path}\nerror:${error}`); + } + + if (file_text == null) { // 检测文件是否读取 + log.error(`读取文件 ${music_path} 错误,文件为空`); + return null; + } + // else { + // log.info(`文件读取成功: ${music_path}`); + // } + + let music_msg_dic = JSON.parse(file_text); + let regex_blank = /[\n]/g; + + MusicInfo.name = (music_msg_dic.name !== undefined) ? (music_msg_dic.name) : ("未知曲名"); + MusicInfo.author = (music_msg_dic.author !== undefined) ? (music_msg_dic.author) : ("未知作者"); + MusicInfo.instrument = (music_msg_dic.instrument !== undefined) ? (music_msg_dic.instrument) : ("无建议乐器"); + MusicInfo.description = (music_msg_dic.description !== undefined) ? (music_msg_dic.description) : ("无描述"); + MusicInfo.composer = (music_msg_dic.composer !== undefined) ? (music_msg_dic.composer) : ("未知作曲者"); + MusicInfo.arranger = (music_msg_dic.arranger !== undefined) ? (music_msg_dic.arranger) : ("未知编曲者"); + // 必要信息 + MusicInfo.type = (music_msg_dic.type !== undefined) ? (music_msg_dic.type) : ("yuanqin"); + MusicInfo.bpm = (music_msg_dic.bpm !== undefined) ? (music_msg_dic.bpm) : (120); + MusicInfo.time_signature = (music_msg_dic.time_signature !== undefined) ? (music_msg_dic.time_signature) : ("4/4"); + + if (music_msg_dic.notes === undefined) { + log.error(`文件 ${music_name} 无乐曲信息`); + return null; + } + + switch (MusicInfo.type) { + case "yuanqin": + MusicInfo.notes = parseMusicSheet(music_msg_dic.notes.replace(regex_blank, "")); + break; + case "midi": + MusicInfo.notes = music_msg_dic.notes; + break; + case "keyboard": + MusicInfo.notes = keySheetSerialization(music_msg_dic.notes); + default: + break; + } + + return MusicInfo; + } /** * * 执行单音 @@ -42,181 +230,236 @@ } /** - * - * 根据乐曲文件名生成乐曲文件路径 - * - * @param music_name 乐曲文件名 - * @returns {string} 乐曲文件路径 + * 音符小节序列演奏 + * @typedef {[Number,[Map]]} Bar + * @param {Bar[]} bar_list + * @param {Number} gap 一拍的时长,单位ms + * @property {Number} barTime 小节时长 + * @property {[Map]} notes 一个小节中所有音符的信息 */ - function path_join(music_name) { - return base_path + music_name + ".json"; + async function listNotePlay(bar_list, gap) { + /** + * 按键模拟 + * 不使用await修饰调用,利用javascript特性实现异步弹奏 + * + * @param {Map} note + * @param {Number} gap + * @description offset:小节开始时此音符需要先等待多久,单位为一拍时间 + * @description key:键盘按键 + * @description time:此音符需要持续的时长,单位为一拍时间 + */ + async function notePlay(note, gap) { + const wait = note["offset"]; + const key = note["key"]; + const time = note["time"]; + await sleep(Math.floor(wait * gap)); + keyDown(key); + await sleep(Math.floor(time * gap)); + keyUp(key); + } + log.info(`总计 ${bar_list.length} 小节, 预计演奏时长 ${bar_list.length * gap * 4 / 1000}秒`); + for (let i = 0; i < bar_list.length; i++) { + let bar = bar_list[i]; + let barTime = bar[0]; + let notes = bar.slice(1); + for (let j = 0; j < notes.length; j++) { + let note = notes[j]; + notePlay(note, gap); // 启动音符异步函数 + } + await sleep(barTime * gap); // 等待小节结束 + } + await sleep(gap * 8); // 额外等待 } /** - * - * 计算当前音符的时长(检测音符后是否有装饰音) - * - * @param sheet_list {Object[][]} 解析后的乐谱 - * @param symbol_time 每一拍的时间 - * @param symbol 以几分音符为一拍 - * @param note_type 音符类型 - * @param count 当前音符下标 - * @param note_time 当前音符的时长(默认为undefined,不为空时symbol note_type count实效) - * @returns {number} + * 将乐谱键位字符串序列化为按小节分组的音符对象数组 + * + * 此函数处理自定义记谱字符串,将其解析为音符组,展开嵌套组,合并相邻音符,并按小节分组 + * 每个小节表示为一个数组,首元素为小节长度(固定为4),后接包含键位、偏移量和时间属性的音符对象 + * + * @param {string} stringSheet - 待序列化的键位乐谱字符串 + * @returns {Array>} - 小节数组,每个小节为数组结构(首元素为长度4,后接音符对象): + * - { key: string, offset: number, time: number } + * + * @example + * const testString = "(QH) DQ/D-G-/[(HF)A] A /FH(QH) / (QG)SJ>(JG)Q(WJ)G>[G0E00]/-(EA)-DF/(GD)H(GD)F/"; + * const result = keySheetSerialization(testString); + * // 返回结果: [ + * // [4, { key: 'Q', offset: 0, time: 1 }, ...], + * // [4, { key: 'D', offset: 0, time: 2 }, ...], + * // ... + * // ] */ - function cal_time_ornament(sheet_list, symbol_time, symbol, note_type, count, note_time=undefined) { - try { - if (note_time === undefined) { - // 该音符的正常时长 - note_time = Math.round(symbol_time * (symbol / note_type)); - } - // 装饰音时长 - let ornament_time = Math.round(symbol_time / 16) + function keySheetSerialization(stringSheet) { + /** + * 函数是安全的,在处理按键序列时不会触发回溯地狱 + * @param {String} inputString + * @example + * const testString = "(QH) DQ/D-G-/[(HF)A] A /FH(QH) / (QG)SJ>(JG)Q(WJ)G>[G0E00]/-(EA)-DF/(GD)H(GD)F/"; + * console.log("原始字符串:", testString); + * console.log("转换后字符串:", keySheetProcess(testString)); + * input : "(QH) DQ/D-G-/[(HF)A] A /FH(QH) / (QG)SJ>(JG)Q(WJ)G>[G0E00]/-(EA)-DF/(GD)H(GD)F/" + * output : "(QH)0DQ{D}-G-{[(HF)A]}0A0{F}H(QH)00(QG)SJ(JG)Q(WJ)G[G0E00]-(EA)-DF{GD}H(GD)F" + */ + const keySheetProcess = (inputString) => { + return inputString + .replace(/\/\(([^)]+)\)/g, '{$1}') // 替换 /(content) 为 {content} + .replace(/\/([A-Z])/g, '{$1}') // 替换 /X 为 {X} + .replace(/ /g, "0") // 替换空格为 0 + .replace(/\/\[([^\]]+)\]/g, '{[$1]}') // 替换 /[content] 为 {[content]} + .replace(/[\/\>]/g, ""); // 删除所有 / 和 > + }; - let check_count = count + 1; - let ornament_count = 0; // 装饰音计数 - - while (check_count < sheet_list.length) { // 装饰音不可能在曲谱末尾,else会在匹配不到装饰音的循环触发 - if (sheet_list[check_count]["spl"] === "#") { - ornament_count += 1; - } else { - if (ornament_count === 0) { - return note_time; - } else { - // 装饰音占用的时间过长就不预留时间 - if (ornament_time * ornament_count < note_time) { - return note_time - ornament_time * ornament_count; - } else { - return note_time; + /** + * @typedef {Array} noteInfo + * @param {String} processedString 处理完成的字符串,只有A-Z,0,-,()[]{} + * @returns {[noteInfo[]]} + */ + const keySheetParse = (processedString) => { + const isLeftBrackets = (char) => ((char.length === 1) && (/[\(\[\{]/.test(char))); + const isRightBrackets = (char) => ((char.length === 1) && (/[\)\]\}]/.test(char))); + class GroupProcess { + constructor() { + this.stack = [{ type: 'ROOT', listKey: [] }]; + this.current = this.stack[0]; + } + push(char) { + if (isLeftBrackets(char)) { + const newGroup = { type: char, listKey: [] }; + this.current.listKey.push(newGroup); + this.stack.push(newGroup); + this.current = newGroup; + } + else if (isRightBrackets(char)) { + if (this.stack.length > 1) { + this.stack.pop(); + this.current = this.stack[this.stack.length - 1]; } } - } - check_count += 1; - } - } catch (error) { - log.error(`出错(cal_time_ornament): ${error}`); - } - - } - - /** - * 获取JS脚本配置 - * - * @returns {Object} 包含解析后JS脚本配置的对象,具有以下属性: - * - type {string} 执行类型:单曲和队列 - * - repeat {integer} 单曲重复次数 - * - interval {integer} 队列间隔时间(type为"single"时无此属性) - * - music {string}|{Array.string} 乐曲名(type为"single"时为 {string}, type为"queue"时为 {Array.}) - * - */ - function get_settings(music_list) { - try{ - // 读取开始时间 - let music_start = typeof(settings.music_start) === 'undefined' ? "" : settings.music_start; - // 读取选择的单曲 - let music_single = typeof(settings.music_selector) === 'undefined' ? 0 : settings.music_selector; - // 读取循环次数 - let music_repeat = typeof(settings.music_repeat) === 'undefined' ? 1 : parseInt(settings.music_repeat, 10); - // 读取循环间隔时间 - let repeat_interval = typeof(settings.repeat_interval) === 'undefined' ? 0 : parseInt(settings.repeat_interval, 10); - // 读取循环模式 - let repeat_mode = typeof(settings.repeat_mode) === 'undefined' ? "单曲循环" : settings.repeat_mode; - // 读取乐曲队列 - let music_queue = typeof(settings.music_queue) === 'undefined' ? 0 : settings.music_queue; - // 读取队列间隔时间 - let music_interval = typeof(settings.music_interval) === 'undefined' ? 0 : parseInt(settings.music_interval, 10); - - let local_music_dic = {}; // 存储本地乐曲对照字典 - // 写入本地乐曲对照字典 - for (const each of music_list) { - if (each !== "example") { - // 从文件名提取编号 - let temp_num = each.split(".")[0]; - local_music_dic[temp_num] = each; - } - } - - if (music_queue === 0 || music_queue === "") { // 单曲执行 - if (music_single !== 0) { - return { - "type": "single", - "start": music_start, - "repeat": music_repeat, - "repeat_interval": repeat_interval, - "music": local_music_dic[music_single.split(".")[0]] - }; - } else { - log.error(`错误:JS脚本配置有误(单曲未选择)`); - return null; - } - } else { // 队列执行 - let temp_music_list = []; // 存储乐曲名 - - // 读取乐曲队列配置 - for (const num of music_queue.split(" ")) { - if (Object.keys(local_music_dic).includes(num)) { - temp_music_list.push(local_music_dic[num]); - log.info(`乐曲: ${local_music_dic[num]} 已加入队列`) - } else { - log.info(`编号不存在,已跳过(编号:${num})`) + else if (char !== '-') { + this.current.listKey.push(char); } - + return this; + } + invaildMatch() { return ((this.stack.length === 1) && (this.stack[0].listKey.length !== 0)); } + clear() { + this.stack = [{ type: 'ROOT', listKey: [] }]; + this.current = this.stack[0]; + } + genAll() { + let out = this.stack[0].listKey[0]; + if ((typeof out) === "string") out = { type: "{", listKey: [out] }; + out.mult = 1; + this.clear(); + return out; } - return { - "type": "queue", - "start": music_start, - "repeat": music_repeat, - "repeat_interval": repeat_interval, - "repeat_mode": repeat_mode, - "interval": music_interval, - "music": temp_music_list - }; } - } catch (error) { - log.error(`读取JS脚本配置时出错:${error}`); - } - } - /** - * - * 读取并解析一个乐谱文件 - * - * @param music_name {string} 乐曲文件名 - * @returns {Promise<{}|null>} - */ - async function get_music_msg(music_name) { + let group = new GroupProcess(); // 处理流程1 + let groupProess = new Array(); // 处理流程2 + for (let i = 0; i < processedString.length; i++) { + const char = processedString[i]; + if (char !== "-") { group.push(char); } + else { groupProess[groupProess.length - 1].mult += 1; } - let music_path = path_join(music_name); - let file_text = ""; // 存储乐曲文件内容 - - try{ - file_text = file.readTextSync(music_path); - } catch (error) { - log.error(`文件无法读取:${music_path}\nerror:${error}`); - } - - if(file_text == null){ // 检测文件是否读取 - log.error(`读取文件 ${music_path} 错误,文件为空`); - return null ; - } else { - log.info(`文件读取成功: ${music_path}`); - } - - let music_msg_dic = JSON.parse(file_text); - let regex_blank = /[\n]/g - try { - // 曲谱内容(删除换行符) - if (music_msg_dic["author"] !== "MidiTrans") { - music_msg_dic["notes"] = music_msg_dic["notes"].replace(regex_blank, ''); - } else { - music_msg_dic["notes"] = JSON.parse(music_msg_dic["notes"])["notes"]; + if (group.invaildMatch()) { groupProess.push(group.genAll()); } } - } catch(error) { - log.info(`曲谱解析错误:${error}\n请检查曲谱文件格式是否正确`); - return null; + // console.dir(groupProess, { depth: null }); + return groupProess; } - return music_msg_dic; + function unfoldGroup(input) { + const unfoldGroup = []; + let cumulativeBeats = 0; + + const processGroup = (group, mult, beats, baseOffset) => { + let offset = baseOffset; + if (group.type === '{') offset += 0.001; + + if (group.type === '[') { + const unitTime = mult / group.listKey.length; + group.listKey.forEach((item, i) => { + const itemOffset = offset + i * unitTime; + if (typeof item === 'string') { + if (item !== '0') unfoldGroup.push({ beats, offset: itemOffset, key: item, time: unitTime }); + } else { + processGroup(item, unitTime, beats, itemOffset); + } + }); + } else { + group.listKey.forEach(item => { + if (typeof item === 'string') { + if (item !== '0') unfoldGroup.push({ beats, offset, key: item, time: mult }); + } else { + processGroup(item, mult, beats, offset); + } + }); + } + }; + + input.forEach(group => { + const groupBeats = cumulativeBeats; + cumulativeBeats += group.mult; + processGroup(group, group.mult, groupBeats, 0); + }); + + return unfoldGroup; + } + + function mergeGroup(notes) { + const buckets = {}; + + notes.forEach(note => { + if (!buckets[note.key]) { + buckets[note.key] = []; + } + buckets[note.key].push({ ...note }); + }); + + const mergedNotes = []; + + Object.keys(buckets).forEach(key => { + const bucket = buckets[key]; + bucket.sort((a, b) => (a.beats + a.offset) - (b.beats + b.offset)); + + let i = 0; + while (i < bucket.length - 1) { + const current = bucket[i]; + const next = bucket[i + 1]; + const currentEnd = current.beats + current.time; + const nextStart = next.beats + next.offset; + + if (Math.abs(currentEnd - nextStart) < 0.01) { + current.time += next.time; + bucket.splice(i + 1, 1); + } else { + i++; + } + } + + mergedNotes.push(...bucket); + }); + + mergedNotes.sort((a, b) => { + if (a.beats !== b.beats) return a.beats - b.beats; + return a.offset - b.offset; + }); + + return mergedNotes; + } + let SerializedKey = keySheetProcess(stringSheet); + SerializedKey = keySheetParse(SerializedKey); + SerializedKey = unfoldGroup(SerializedKey); + SerializedKey = mergeGroup(SerializedKey); + const grouped = []; + const wholeBeats = Math.floor(SerializedKey[SerializedKey.length - 1].beats / 4) + 1; + for (let i = 0; i < wholeBeats; i++) { + grouped.push([4]); + } + for (const note of SerializedKey) { + grouped[Math.floor(note.beats / 4)].push({ offset: note.beats % 4 + note.offset, key: note.key, time: note.time }); + } + return grouped; } /** @@ -237,7 +480,7 @@ function parseMusicSheet(sheet) { let result = []; - if (typeof(sheet) === "object") { + if (typeof (sheet) === "object") { result = sheet; } else { // 将输入字符串按照小节分割 @@ -313,6 +556,53 @@ * @returns {Promise} */ async function play_sheet(sheet_list, bpm, ts) { + /** + * + * 计算当前音符的时长(检测音符后是否有装饰音) + * + * @param sheet_list {Object[][]} 解析后的乐谱 + * @param symbol_time 每一拍的时间 + * @param symbol 以几分音符为一拍 + * @param note_type 音符类型 + * @param count 当前音符下标 + * @param note_time 当前音符的时长(默认为undefined,不为空时symbol note_type count实效) + * @returns {number} + */ + function cal_time_ornament(sheet_list, symbol_time, symbol, note_type, count, note_time = undefined) { + try { + if (note_time === undefined) { + // 该音符的正常时长 + note_time = Math.round(symbol_time * (symbol / note_type)); + } + // 装饰音时长 + let ornament_time = Math.round(symbol_time / 16) + + let check_count = count + 1; + let ornament_count = 0; // 装饰音计数 + + while (check_count < sheet_list.length) { // 装饰音不可能在曲谱末尾,else会在匹配不到装饰音的循环触发 + if (sheet_list[check_count]["spl"] === "#") { + ornament_count += 1; + } else { + if (ornament_count === 0) { + return note_time; + } else { + // 装饰音占用的时间过长就不预留时间 + if (ornament_time * ornament_count < note_time) { + return note_time - ornament_time * ornament_count; + } else { + return note_time; + } + } + } + check_count += 1; + } + } catch (error) { + log.error(`出错(cal_time_ornament): ${error}`); + } + + } + // 如果是midi转换的乐谱 if (Object.keys(sheet_list[0]).length === 3) { for (let i = 0; i < sheet_list.length; i++) { await sleep(Math.round(sheet_list[i]["time"])); @@ -423,130 +713,108 @@ } } - async function main() { - // 首先检测本地曲谱文件与主程序中是否一致(本地已有的曲谱为最高优先级) - // 1.读取本地所有JSON曲谱文件 - let music_list = []; - const all_scores = Array.from(file.readPathSync("assets/score_file")).filter(p => !file.isFolder(p) && p.endsWith(".json")); - for (let i = 0; i < all_scores.length; i++) { - music_list.push(all_scores[i].match(regex_name)[0]); + async function waitTargetTime(targetTimeStamp) { + let now = new Date(); + if (now.getTime() >= targetTimeStamp) return; + log.info(`等待至目标时间: ${new Date(targetTimeStamp).toLocaleString()}`); + if ((targetTimeStamp - now.getTime()) > 100) { + log.info(`${Math.floor((targetTimeStamp - now.getTime()) / 1000)}秒后开始`); + await sleep(targetTimeStamp - now.getTime() - 100); } - // 2.读取JS脚本配置中的曲谱列表 - let setting_list = []; - let ori_set_list = JSON.parse(file.readTextSync("settings.json"))[1]["options"]; - for (let i = 0; i < ori_set_list.length; i++) { - setting_list.push(ori_set_list[i]); + while (Date.now() < targetTimeStamp) { } - // 3.核对 - if (!(setting_list.sort().join() == music_list.sort().join())) { // 曲谱配置不相同 - // 以本地曲谱为准 - let temp_json = JSON.parse(file.readTextSync("settings.json")); - temp_json[0]["options"] = music_list; - file.writeTextSync("settings.json", JSON.stringify(temp_json, null, 2)); // 覆写settings - log.warn("检测到曲谱文件不一致,已自动修改settings(以本地曲谱文件为基准)..."); - log.warn("JS脚本配置已更新,请重新运行脚本!"); - return null; - } - const settings_msg = get_settings(music_list); - // 检测开始时间 - // if (settings_msg["start"] !== "") { - // let target_time = new Date(); - // for (let i = 0; i < settings_msg["start"].length; i++) { - // if (i == 0) { - // time_target.setHours(parseInt(settings_msg["start"][i], 10)); - // time_target.setMinutes(0); - // time_target.setSeconds(0); - // time_target.setMilliseconds(0); - // } else if (i == 1) { - // time_target.setMinutes(parseInt(settings_msg["start"][i], 10)); - // time_target.setSeconds(0); - // time_target.setMilliseconds(0); - // } else if (i == 2) { - // time_target.setSeconds(parseInt(settings_msg["start"][i], 10)); - // time_target.setMilliseconds(0); - // } else if (i == 3) { - // time_target.setMilliseconds(parseInt(settings_msg["start"][i], 10)); - // } - // } - // } - let time_target = new Date(); - if (settings_msg.start !== "") { - const setters = ["setHours", "setMinutes", "setSeconds", "setMilliseconds"]; - let start_time_list = settings_msg.start.split(":"); - start_time_list.forEach((val, i) => { - time_target[setters[i]](parseInt(val, 10)); - // 清零更小的单位 - for (let j = i + 1; j < setters.length; j++) { - time_target[setters[j]](0); - } - }); - - // 如果剩余时间大于 1 秒,先等待到目标时间前 1 秒 - let diff = time_target - new Date(); - if (diff > 1000) await sleep(diff - 1000); - // 最后 1 秒内用短间隔检查 - while (new Date() < time_target) { - continue; - } - } - - // if (settings_msg == null) { - // return null - // } - - // try { - if (settings_msg["type"] === "single") { // 单曲 - // 读取乐谱 - const music_msg = await get_music_msg(settings_msg["music"]); - const music_sheet = parseMusicSheet(music_msg["notes"]); - - for (let i = 0; i < settings_msg["repeat"]; i++) { - await play_sheet(music_sheet, music_msg["bpm"], music_msg["time_signature"]); - - // 单曲循环间隔 - if (settings_msg["repeat"] !== 1 && i !== settings_msg["repeat"] - 1) { - await sleep(settings_msg["repeat_interval"] * 1000); - } - } - } else { // 队列 - // 存储读取的乐谱 - let music_msg_list = []; - // 读取乐谱 - for (let i = 0; i < settings_msg["music"].length; i++) { - const music_msg = await get_music_msg(settings_msg["music"][i]); - const music_sheet = parseMusicSheet(music_msg["notes"]); - - music_msg_list.push([settings_msg["music"][i], music_msg, music_sheet]); - } - - let repeat_queue = settings_msg["repeat_mode"] === "队列循环" ? settings_msg["repeat"] : 1; - let repeat_single = settings_msg["repeat_mode"] !== "队列循环" ? settings_msg["repeat"] : 1; - - for (let r = 0; r < repeat_queue; r++) { - for (let j = 0; j < music_msg_list.length; j++) { - for (let i = 0; i < repeat_single; i++) { - await play_sheet(music_msg_list[j][2], music_msg_list[j][1]["bpm"], music_msg_list[j][1]["time_signature"]); - log.info(`曲目: ${music_msg_list[j][0]} 演奏完成`); - if (repeat_single !== 1) { - await sleep(settings_msg["repeat_interval"] * 1000); // 单曲循环间隔 - } - } - // 队列内间隔 - if (music_msg_list.indexOf(music_msg_list[j]) !== music_msg_list.length - 1) { - await sleep(settings_msg["interval"] * 1000); - } - } - // 队列循环间隔 - if (repeat_queue !== 1 && r !== repeat_queue - 1) { - await sleep(settings_msg["repeat_interval"] * 1000); - } - } - } - // } catch (error) { - // log.error(`出现错误: ${error}`); - // } - + return; } + /** + * 检查本地曲谱文件与主程序配置是否一致,并自动修正配置文件。 + * + * @returns {boolean} 如果一致返回 true,否则返回 false。 + */ + function checkSheetFile() { + try { + // 1. 读取本地所有JSON曲谱文件 + const localMusicList = musicList(); + + // 2. 读取JS脚本配置中的曲谱列表 + const settings = JSON.parse(file.readTextSync("settings.json")); + let configMusicList = undefined; + let indexOfMusicSelector = -1; + for (let i = 0; i < settings.length; i++) { + if (settings[i].name === "music_selector") { + indexOfMusicSelector = i; + configMusicList = settings[i].options; + break; + } + } + // 3. 核对两个列表是否相同 + const areArraysEqual = (a, b) => { + if (a.length !== b.length) return false; + const sortedA = [...a].sort(); + const sortedB = [...b].sort(); + return sortedA.every((item, index) => item === sortedB[index]); + }; + + if (!areArraysEqual(localMusicList, configMusicList)) { + // 以本地曲谱为准更新配置 + const updatedSettings = [...settings]; + updatedSettings[indexOfMusicSelector].options = localMusicList; + file.writeTextSync("settings.json", JSON.stringify(updatedSettings, null, 2)); + log.warn("检测到曲谱文件不一致, 已自动修改settings(以本地曲谱文件为基准)..."); + log.warn("JS脚本配置已更新, 请重新运行脚本!"); + return false; + } + + return true; + } catch (error) { + log.error("检查曲谱文件时发生错误:", error); + return false; + } + } + + /** + * ------- 主程序 -------- + */ + async function main() { + if (!checkSheetFile()) return; + + let settings_msg = get_settings(); + console.log(`${settings_msg}`) + file.writeTextSync("last_settings.json", `${settings_msg}`); + + const music_infos = []; + for (const music_name of settings_msg.musicQueue) { + const music_info = getMusicInfo(music_name); + if (music_info === null) { + log.error(`乐曲 ${music_name} 信息有误,已跳过`); + continue; + } + music_infos.push(music_info); + } + + + const alwaysRepeat = ((settings_msg.playType === PlayType.SingleMusicRepeat || settings_msg.playType === PlayType.QueueMusicRepeat) && (settings_msg.repeatTimes === 0)); + await waitTargetTime(settings_msg.startTime); + do { + for (const music_info of music_infos) { + log.info(`开始演奏: ${music_info.name} - ${music_info.author}`); + switch (music_info.type) { + case "yuanqin": + await play_sheet(music_info.notes, music_info.bpm, music_info.time_signature); + break; + case "midi": + await play_sheet(music_info.notes, music_info.bpm, music_info.time_signature); + break; + case "keyboard": + await listNotePlay(music_info.notes, (60000 / music_info.bpm)); + break; + default: + break; + } + if (settings_msg.queueInterval > 0) await sleep(settings_msg.queueInterval * 1000); + } + if (settings_msg.repeatInterval > 0) await sleep(settings_msg.repeatInterval * 1000); + } while (alwaysRepeat || --settings_msg.repeatTimes > 0); + } await main(); })(); \ No newline at end of file diff --git a/repo/js/AutoYuanQin/manifest.json b/repo/js/AutoYuanQin/manifest.json index f9856eabf..0f169a57e 100644 --- a/repo/js/AutoYuanQin/manifest.json +++ b/repo/js/AutoYuanQin/manifest.json @@ -1,13 +1,17 @@ { "manifest_version": 1, "name": "原琴·五线谱版", - "version": "3.0.1", + "version": "3.1.0", "bgi_version": "0.43.1", "description": "功能描述:功能及其强大的原琴脚本\n核心功能------------------------------>\n1.轻松实现根据五线谱翻版琴谱,支持单音、和弦\n2.曲谱支持录入BPM、拍号\n3.特殊音符支持休止符、浮点音符、(三/六)连音、(三/六)连音标记线、装饰音·倚音\n4.含有制谱器,方便制作曲谱\n注意事项------------------------------>\n1.使用前请装备原琴\n2.音域只有3个八度,受原琴音域限制,本脚本的上限取决于翻谱的大佬(卑微\n3.实际上装饰音·倚音的时长视为基础时值单位(比如拍号2/4的基础时值单位就是4分音符)的1/16\n4.制铺说明:曲谱JSON文件的notes必须保证为一行且不能包括空白符(换行符除外);小节之间用|隔开,|不是必要的,作用是方便曲谱维护\n---------------------------------------->\n作者:提瓦特钓鱼玳师\n脚本反馈邮箱:hijiwos@hotmail.com", "authors": [ { "name": "提瓦特钓鱼玳师", "links": "https://github.com/Hijiwos" + }, + { + "name": "半江残秋", + "links": "https://github.com/TheHeartFickle" } ], "settings_ui": "settings.json", diff --git a/repo/js/AutoYuanQin/settings.json b/repo/js/AutoYuanQin/settings.json index 9f09b9efe..4d3ca8069 100644 --- a/repo/js/AutoYuanQin/settings.json +++ b/repo/js/AutoYuanQin/settings.json @@ -1,13 +1,24 @@ [ - { - "name": "music_start", - "type": "input-text", - "label": "开始时间(留空则不启用,示例 00:00:00 )" - }, + { + "name": "music_start", + "type": "input-text", + "label": "定时启动时间(示例 19:19:10 默认:不启用)" + }, + { + "name": "type_select", + "type": "select", + "label": "播放模式(默认:单曲单次执行)", + "options": [ + "单曲单次执行", + "单曲循环", + "队列单次执行", + "队列循环" + ] + }, { "name": "music_selector", "type": "select", - "label": "选择乐曲(队列执行启用后该选项失效)", + "label": "选择乐曲(单曲模式中有效)", "options": [ "0001.小星星", "0002.小星星变奏曲", @@ -18,26 +29,8 @@ "0007.千本樱 (Eric Chen)", "0008.春よ、来い(春天,来吧)", "0009.One Last Kiss", - "0010.卡农(MIDI转谱)" - ] - }, - { - "name": "music_repeat", - "type": "input-text", - "label": "循环执行次数(不填默认不循环)" - }, - { - "name": "repeat_interval", - "type": "input-text", - "label": "循环间隔时间(单位:s, 循环执行启用时生效,不填默认0s)" - }, - { - "name": "repeat_mode", - "type": "select", - "label": "循环模式(不填默认为单曲循环)", - "options": [ - "单曲循环", - "队列循环" + "0010.卡农(MIDI转谱)", + "0011.圆号卡农" ] }, { @@ -48,6 +41,16 @@ { "name": "music_interval", "type": "input-text", - "label": "队列内间隔时间(单位:s, 队列执行启用时生效)" + "label": "队列内间隔时间(单位:s, 队列模式中有效)" + }, + { + "name": "music_repeat", + "type": "input-text", + "label": "循环执行次数(值为0时无限循环 默认:1)" + }, + { + "name": "repeat_interval", + "type": "input-text", + "label": "循环间隔时间(单位:s, 循环模式下生效,默认:0s)" } ] \ No newline at end of file