AutoYuanQin新版适配,优化了定时播放和配置更新,简化了代码
This commit is contained in:
NuperAki
2025-09-11 08:39:38 +08:00
committed by GitHub
parent ff1bc0fb88
commit 509f39decf
8 changed files with 816 additions and 502 deletions

View File

@@ -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 v71029539994群主帮你更新到仓库
### 本地播放
1. 如果你暂时不将乐谱上传至仓库, 那么你可以在"单曲名称"中将你置于`AutoYuanQin/assets`下的乐谱名称输入, 此自定义配置的优先级高于单曲选择
3.发送邮件到hijiwos@hotmail.com并说明你的谱子将会在一段时间内更新到仓库
## MIDI翻谱器 <a id="MIDI翻谱器"></a>
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**: 曲谱的BPMBeats 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键视作四分音符。
<div align="center">
<img src="assets\tutorial_file\四分音符示例.png"/>
<p>四分音符示例</p>
</div>
`notes`: **必要键值** 曲谱内容,具体格式可参考以下解析规则
### F[16-#]D[16-#]S[16-#]
表示**装饰音·倚音**
<div align="center">
<img src="assets\tutorial_file\装饰音·倚音示例.png"/>
<p>装饰音·倚音示例</p>
</div>
以上每个装饰音的时值固定为拍号中的标准时值(3/4的标准时值为四分音符的时值)的1/16也就是说以上示例中的**16没有意义但是必须要写**
### Z[4-8.3]C[4-8.3]B[4-8.$]
表示一个**三连音**六连音用法与此相似仅需将3改成6**其它类型的连音**也请使用3或6(即使是5连音)
另外,连音内支持和弦
<div align="center">
<img src="assets\tutorial_file\三连音示例.png"/>
<p>三连音示例</p>
</div>
* 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连音**
<div align="center">
<img src="assets\tutorial_file\五连音示例.png"/>
<p>五连音示例</p>
</div>
* D[4-16.3]
4表示该连音的总时值相当于四分音符16表示当前音符在乐谱上显示的时值相当于十六分音符的时值3表示这个音符是一个连音的一部分
* R[4-16.$]
$表示这是当前连音的最后一个音符
### (BG)[4-4.3]\(VF\)[4-8.$]
表示一个**三连音连音线**(与三连音用法相同,但是三连音连音线允许连线内出现不同类型的音符)
<div align="center">
<img src="assets\tutorial_file\三连音连音线示例.png"/>
<p>三连音连音线示例</p>
</div>
* (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
<div align="center">
<img src="assets\tutorial_file\六连音连音线示例.png"/>
<p>六连音连音线示例</p>
</div>
* @[2-8.3]
2表示该六连音的总时值相当于一个二分音符8表示当前音符在乐谱上显示的时值相当于八分音符的时值6表示这是一个六连音
* N[2-16.$]
16表示当前音符在乐谱上显示的时值相当于十六分音符的时值$表示这是当前连音的最后一个音符
### @[4]
表示一个**休止符**
<div align="center">
<img src="assets\tutorial_file\四分休止符示例.png"/>
<p>四分休止符示例</p>
</div>
中括号内表明这是几分休止符,例如这里表示四分休止符。
### (SH)[4-*]
表示一个**附点四分音符**
<div align="center">
<img src="assets\tutorial_file\附点四分音符示例.png"/>
<p>附点四分音符示例</p>
</div>
表示按下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 解析规则 <a id="解析规则"></a>
notes 字段中包含的是乐谱内容音符**必须**使用**大写字母**, 乐谱内容使用字符串表示, 小节之间用 | 隔开单个小节的解析规则如下:
### `A[4]`
表示按下A键, A键视作四分音符
<div align="center">
<img src="assets/tutorial_file/四分音符示例.png"/>
<p>四分音符示例</p>
</div>
### `F[16-#]D[16-#]S[16-#]`
表示**装饰音·倚音**
<div align="center">
<img src="assets/tutorial_file/装饰音·倚音示例.png"/>
<p>装饰音·倚音示例</p>
</div>
以上每个装饰音的时值固定为拍号中的标准时值(3/4的标准时值为四分音符的时值)的1/16, 也就是说以上示例中的**16没有意义, 但是必须要写**
### `Z[4-8.3]C[4-8.3]B[4-8.$]`
表示一个**三连音**(六连音用法与此相似, 仅需将3改成6, **其它类型的连音**也请使用3或6(即使是5连音))
另外, 连音内支持和弦
<div align="center">
<img src="assets/tutorial_file/三连音示例.png"/>
<p>三连音示例</p>
</div>
* `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连音**
<div align="center">
<img src="assets/tutorial_file/五连音示例.png"/>
<p>五连音示例</p>
</div>
* `D[4-16.3]`
4表示该连音的总时值相当于四分音符, 16表示当前音符在乐谱上显示的时值相当于十六分音符的时值, 3表示这个音符是一个连音的一部分
* `R[4-16.$]`
$表示这是当前连音的最后一个音符
### `(BG)[4-4.3]/(VF/)[4-8.$]`
表示一个**三连音连音线**(与三连音用法相同, 但是三连音连音线允许连线内出现不同类型的音符)
<div align="center">
<img src="assets/tutorial_file/三连音连音线示例.png"/>
<p>三连音连音线示例</p>
</div>
* (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)
<div align="center">
<img src="assets/tutorial_file/六连音连音线示例.png"/>
<p>六连音连音线示例</p>
</div>
* `@[2-8.3]`
2表示该六连音的总时值相当于一个二分音符, 8表示当前音符在乐谱上显示的时值相当于八分音符的时值, 6表示这是一个六连音
* `N[2-16.$] `
16表示当前音符在乐谱上显示的时值相当于十六分音符的时值, $表示这是当前连音的最后一个音符
### `@[4]`
表示一个**休止符**
<div align="center">
<img src="assets/tutorial_file/四分休止符示例.png"/>
<p>四分休止符示例</p>
</div>
中括号内表明这是几分休止符, 例如这里表示四分休止符
### `(SH)[4-*]`
表示一个**附点四分音符**
<div align="center">
<img src="assets/tutorial_file/附点四分音符示例.png"/>
<p>附点四分音符示例</p>
</div>
表示按下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]"
}
```
- ver 3.1.1
1. 新增了解析基于字符串的按键序列的函数
1. 新增了执行上述函数序列化按键信息的函数
1. 初步完成了以上函数与原项目代码的适配
1. 制谱软件从assets中转移至脚本根路径的./tools文件夹下
1. 五线谱制谱软件的文件夹名称改为StaffMaker
1. MIDI翻谱器从五线谱制谱器路径中移出
1. 修改manifest.name为"原琴-音乐转换·自动演奏"
1. 修改settings.json, 使其更易被理解
1. 增加作者
1. 修改了README.md

View File

@@ -2,6 +2,7 @@
"name": "示例曲谱",
"author": "MidiTrans",
"bpm": "120",
"type": "midi",
"description": "曲谱信息",
"time_signature": "4/4",
"composer": "曲师",

View File

@@ -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)---"
}

View File

@@ -194,6 +194,7 @@ function parseMidi(arrayBuffer) {
bpm: Math.round(60000000 / tempo).toString(),
description: "曲谱信息",
time_signature: "4/4",
type:"midi",
composer: "曲师",
arranger: "谱师",
notes: mappedNotes

View File

@@ -0,0 +1 @@
[object Object]

View File

@@ -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<Array<number|Object>>} - 小节数组,每个小节为数组结构(首元素为长度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-Z0-()[]{}
* @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.<string>}
*
*/
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<void>}
*/
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();
})();

View File

@@ -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",

View File

@@ -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"
}
]