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