mirror of
https://github.com/TriM-Organization/Musicreater.git
synced 2026-07-24 17:22:32 +00:00
Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8e1afb507d | |||
| 62db7e585f | |||
| debd42c633 | |||
| df3519ebc5 | |||
| 2804760c03 | |||
| 813ffab782 | |||
| b01fccbf4b | |||
| 6b0d88ad5b | |||
| fc9ec86adb | |||
| 2c6e831533 | |||
| 026c529182 | |||
| 2919722c52 | |||
| 4d780b4922 | |||
| 7a03511cc9 | |||
| 3b6d786346 | |||
| 816f3cbc3c | |||
| 6940a211b4 | |||
| 8cd6865bf4 | |||
| 825d275a5e | |||
| de830262a7 | |||
| 9c1383360b | |||
| 60cbdfb9d0 | |||
| d6944392cd | |||
| ba7b10a25f | |||
| 307feb9b24 | |||
| 6e518dada4 | |||
| 4c036cbd4c | |||
| c310ba5dc3 | |||
| d9c92fa269 | |||
| 00c445f7ad | |||
| 3ee686c712 | |||
| 0e95a1e541 | |||
| bbc67921d6 | |||
| 62cd4a0c94 | |||
| 295da53c60 | |||
| fff8e43f53 | |||
| 2a5ccb8eeb |
+12
-2
@@ -6,8 +6,6 @@
|
||||
# mystuff
|
||||
/*.zip
|
||||
/.vscode
|
||||
/*.mid
|
||||
/*.midi
|
||||
/*.mcpack
|
||||
/*.bdx
|
||||
/*.msq
|
||||
@@ -27,6 +25,18 @@ RES.txt
|
||||
/bgArrayLib
|
||||
/fcwslib
|
||||
test_lyric-mido.py
|
||||
test_doing_things.py
|
||||
TEST-*.*
|
||||
|
||||
# Copyrighted
|
||||
vanilla_assets/
|
||||
/*.mid
|
||||
/*.midi
|
||||
|
||||
# Fack Apple
|
||||
.DS_Store
|
||||
# Fack Microsoft
|
||||
Thumbs.db
|
||||
|
||||
# Byte-compiled / optimized
|
||||
__pycache__/
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
# 汉钰律许可协议,第一版
|
||||
|
||||
**总第一版 第二次修订 · 二〇二四年七月七日编 二〇二五年四月二十六日修订**
|
||||
**总第一版 第三次修订 · 二〇二四年七月七日编 二〇二六年七月三日修订**
|
||||
|
||||
## 一、重要须知
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
|
||||
11. 如果用户对任何民事主体,因其在进行本协议所授权进行的活动中侵犯该用户的专利而提起诉讼,那么根据本协议授予该用户的所有关于此作品的任何其他专利许可将在提起上述诉讼之日起终止。
|
||||
|
||||
12. 如果本作品作为用户的其他作品的不可分割的一部分进行任何民事活动,本协议依旧对本作品(即该用户的其他作品的一部分)生效;若本作品完全融入该用户的其他作品之中而不可独立存在,则该用户需要保证其作品存在与本协议冲突的条款;除非该作品已获得此作品颁发者的特殊许可、或该用户即为此作品颁发者本人。
|
||||
12. 如果本作品作为用户的其他作品的不可分割的一部分进行任何民事活动,本协议依旧对本作品(即该用户的其他作品的一部分)生效;若本作品完全融入该用户的其他作品之中而不可独立存在,则该用户需要保证其作品不存在与本协议冲突的条款;除非该作品已获得此作品颁发者的特殊许可、或该用户即为此作品颁发者本人。
|
||||
|
||||
## 四、使用条件
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
|
||||
## 六、商标相关
|
||||
|
||||
本协议并未授予用户,将颁发者的商标、专属标记或特定产品名称,用于合理的或惯例性的描述或此类声明之外其他任何位置的权利。
|
||||
本协议不允许用户,在合理或惯例性的描述、或者用于此类声明之外,在任何其他位置使用颁发者的商标、专用标记或特定产品名称。
|
||||
|
||||
## 七、免责声明
|
||||
|
||||
|
||||
+11
-6
@@ -2,10 +2,11 @@
|
||||
|
||||
"""
|
||||
音·创
|
||||
是一款免费开源的《我的世界》数字音频支持库。
|
||||
是一款免费、开源、高性能、插件化的《我的世界》数字音频支持库。
|
||||
|
||||
Musicreater (音·创)
|
||||
A free and open-source library for handling with **Minecraft** digital music.
|
||||
A cost free, open-source, blazing fast and plugin-
|
||||
supported library for editing _Minecraft_ digital music.
|
||||
|
||||
版权所有 © 2026 睿乐组织
|
||||
Copyright © 2026 TriM-Organization
|
||||
@@ -27,13 +28,12 @@ https://gitee.com/TriM-Organization/Musicreater/blob/master/LICENSE.md。
|
||||
# 若需转载或借鉴 许可声明请查看仓库目录下的 License.md
|
||||
|
||||
|
||||
__version__ = "3.0.0-alpha"
|
||||
__version__ = "3.0.0-beta"
|
||||
|
||||
__author__ = (
|
||||
("金羿", "Eilles"),
|
||||
("玉衡Alioth", "YuhengAlioth"),
|
||||
("鱼旧梦", "ElapsingDreams"),
|
||||
("偷吃不是Touch", "Touch"),
|
||||
)
|
||||
|
||||
from .paramcurve import ParamCurve, InterpolationMethod, BoundaryBehaviour
|
||||
@@ -45,9 +45,11 @@ from .data import (
|
||||
SoundAtmos,
|
||||
MineNote,
|
||||
CurvableParam,
|
||||
Instrument,
|
||||
DefaultInstrument,
|
||||
)
|
||||
|
||||
from .plugins import load_plugin_module
|
||||
from .plugins import load_plugin_from_module
|
||||
|
||||
from .main import MusiCreater
|
||||
|
||||
@@ -65,7 +67,10 @@ __all__ = [
|
||||
"SoundAtmos",
|
||||
"MineNote",
|
||||
"CurvableParam",
|
||||
"Instrument",
|
||||
# 工程项目相关
|
||||
"load_plugin_module",
|
||||
"load_plugin_from_module",
|
||||
"MusiCreater",
|
||||
# 默认乐器
|
||||
"DefaultInstrument",
|
||||
]
|
||||
|
||||
+53
-52
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
存储 音·创 v3 的插件基类,提供抽象接口以供实际插件使用
|
||||
音·创 v3 的插件基类,提供抽象接口以供实际插件使用
|
||||
"""
|
||||
|
||||
"""
|
||||
@@ -41,6 +41,9 @@ from typing import (
|
||||
Generator,
|
||||
Iterator,
|
||||
Set,
|
||||
Type,
|
||||
Mapping,
|
||||
overload,
|
||||
)
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
@@ -97,6 +100,7 @@ from .data import SingleMusic, SingleTrack
|
||||
# 枚举类
|
||||
# ========================
|
||||
|
||||
|
||||
class PluginTypes(str, Enum):
|
||||
"""插件类型枚举"""
|
||||
|
||||
@@ -110,7 +114,6 @@ class PluginTypes(str, Enum):
|
||||
LIBRARY = "library"
|
||||
|
||||
|
||||
|
||||
# ========================
|
||||
# 数据类
|
||||
# ========================
|
||||
@@ -131,7 +134,7 @@ class PluginConfig(ABC):
|
||||
return {k: v for k, v in self.__dict__.items() if not k.startswith("_")}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> "PluginConfig":
|
||||
def from_dict(cls, data: Mapping[str, Any]) -> "PluginConfig":
|
||||
"""从字典创建配置实例
|
||||
|
||||
参数
|
||||
@@ -174,7 +177,7 @@ class PluginConfig(ABC):
|
||||
with file_path.open("wb") as f:
|
||||
tomli_w.dump(self.to_dict(), f, multiline_strings=False, indent=4)
|
||||
except Exception as e:
|
||||
raise PluginConfigDumpError(e)
|
||||
raise PluginConfigDumpError("插件配置文件无法保存。") from e
|
||||
|
||||
@classmethod
|
||||
def load_from_file(cls, file_path: Path) -> "PluginConfig":
|
||||
@@ -199,7 +202,7 @@ class PluginConfig(ABC):
|
||||
with file_path.open("rb") as f:
|
||||
return cls.from_dict(tomllib.load(f))
|
||||
except Exception as e:
|
||||
raise PluginConfigLoadError(e)
|
||||
raise PluginConfigLoadError("插件配置文件无法加载。") from e
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -207,7 +210,7 @@ class PluginMetaInformation(ABC):
|
||||
"""插件元信息"""
|
||||
|
||||
name: str
|
||||
"""插件名称,应为惟一之名"""
|
||||
"""插件名称"""
|
||||
author: str
|
||||
"""插件作者"""
|
||||
description: str
|
||||
@@ -221,6 +224,21 @@ class PluginMetaInformation(ABC):
|
||||
dependencies: Sequence[str] = tuple()
|
||||
"""插件是否对其他插件存在依赖"""
|
||||
|
||||
def similar_to(self, other: "PluginMetaInformation") -> bool:
|
||||
"""判断两个插件元信息是否相似
|
||||
|
||||
参数
|
||||
====
|
||||
other: PluginMetaInformation
|
||||
待比较的插件元信息
|
||||
|
||||
返回
|
||||
====
|
||||
bool
|
||||
两个插件元信息是否相似
|
||||
"""
|
||||
return (self.name == other.name) and (self.type == other.type)
|
||||
|
||||
|
||||
# ========================
|
||||
# 抽象基类
|
||||
@@ -319,9 +337,7 @@ class MusicInputPluginBase(TopInOutPluginBase, ABC):
|
||||
)
|
||||
|
||||
@abstractmethod
|
||||
def loadbytes(
|
||||
self, bytes_buffer_in: BinaryIO, config: Optional[PluginConfig]
|
||||
) -> "SingleMusic":
|
||||
def loadbytes(self, bytes_buffer_in: BinaryIO, config) -> "SingleMusic":
|
||||
"""从字节流加载数据到完整曲目
|
||||
|
||||
参数
|
||||
@@ -336,10 +352,9 @@ class MusicInputPluginBase(TopInOutPluginBase, ABC):
|
||||
SingleMusic
|
||||
解析得到的完整曲目对象
|
||||
"""
|
||||
...
|
||||
|
||||
pass
|
||||
|
||||
def load(self, file_path: Path, config: Optional[PluginConfig]) -> "SingleMusic":
|
||||
def load(self, file_path: Path, config) -> "SingleMusic":
|
||||
"""从文件加载数据到完整曲目
|
||||
|
||||
参数
|
||||
@@ -373,9 +388,7 @@ class TrackInputPluginBase(TopInOutPluginBase, ABC):
|
||||
)
|
||||
|
||||
@abstractmethod
|
||||
def loadbytes(
|
||||
self, bytes_buffer_in: BinaryIO, config: Optional[PluginConfig]
|
||||
) -> "SingleTrack":
|
||||
def loadbytes(self, bytes_buffer_in: BinaryIO, config) -> "SingleTrack":
|
||||
"""从字节流加载音符数据到单个音轨
|
||||
|
||||
参数
|
||||
@@ -390,9 +403,9 @@ class TrackInputPluginBase(TopInOutPluginBase, ABC):
|
||||
SingleTrack
|
||||
解析得到的单个音轨对象
|
||||
"""
|
||||
pass
|
||||
...
|
||||
|
||||
def load(self, file_path: Path, config: Optional[PluginConfig]) -> "SingleTrack":
|
||||
def load(self, file_path: Path, config) -> "SingleTrack":
|
||||
"""从文件加载音符数据到单个音轨
|
||||
|
||||
参数
|
||||
@@ -426,9 +439,7 @@ class MusicOperatePluginBase(TopPluginBase, ABC):
|
||||
)
|
||||
|
||||
@abstractmethod
|
||||
def process(
|
||||
self, data: "SingleMusic", config: Optional[PluginConfig]
|
||||
) -> "SingleMusic":
|
||||
def process(self, data: "SingleMusic", config) -> "SingleMusic":
|
||||
"""处理完整曲目的数据
|
||||
|
||||
参数
|
||||
@@ -443,7 +454,7 @@ class MusicOperatePluginBase(TopPluginBase, ABC):
|
||||
SingleMusic
|
||||
处理后的完整曲目
|
||||
"""
|
||||
pass
|
||||
...
|
||||
|
||||
|
||||
class TrackOperatePluginBase(TopPluginBase, ABC):
|
||||
@@ -461,9 +472,7 @@ class TrackOperatePluginBase(TopPluginBase, ABC):
|
||||
)
|
||||
|
||||
@abstractmethod
|
||||
def process(
|
||||
self, data: "SingleTrack", config: Optional[PluginConfig]
|
||||
) -> "SingleTrack":
|
||||
def process(self, data: "SingleTrack", config) -> "SingleTrack":
|
||||
"""处理单个音轨的音符数据
|
||||
|
||||
参数
|
||||
@@ -478,7 +487,7 @@ class TrackOperatePluginBase(TopPluginBase, ABC):
|
||||
SingleTrack
|
||||
处理后的单个音轨
|
||||
"""
|
||||
pass
|
||||
...
|
||||
|
||||
|
||||
class MusicOutputPluginBase(TopInOutPluginBase, ABC):
|
||||
@@ -496,9 +505,7 @@ class MusicOutputPluginBase(TopInOutPluginBase, ABC):
|
||||
)
|
||||
|
||||
@abstractmethod
|
||||
def dumpbytes(
|
||||
self, data: "SingleMusic", config: Optional[PluginConfig]
|
||||
) -> BinaryIO:
|
||||
def stream_dump(self, data: "SingleMusic", config) -> Iterator[bytes]:
|
||||
"""将完整曲目导出为对应格式的字节流
|
||||
|
||||
参数
|
||||
@@ -510,15 +517,12 @@ class MusicOutputPluginBase(TopInOutPluginBase, ABC):
|
||||
|
||||
返回
|
||||
====
|
||||
BinaryIO
|
||||
导出后的二进制字节流
|
||||
Iterator[bytes]
|
||||
分块导出的二进制字节串
|
||||
"""
|
||||
pass
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def dump(
|
||||
self, data: "SingleMusic", file_path: Path, config: Optional[PluginConfig]
|
||||
):
|
||||
def dump(self, data: "SingleMusic", file_path: Path, config):
|
||||
"""将完整曲目导出为对应格式的文件
|
||||
|
||||
参数
|
||||
@@ -531,7 +535,9 @@ class MusicOutputPluginBase(TopInOutPluginBase, ABC):
|
||||
插件配置;**可选**
|
||||
"""
|
||||
|
||||
pass
|
||||
with file_path.open("wb") as f:
|
||||
for _bytes in self.stream_dump(data, config):
|
||||
f.write(_bytes)
|
||||
|
||||
|
||||
class TrackOutputPluginBase(TopInOutPluginBase, ABC):
|
||||
@@ -549,9 +555,7 @@ class TrackOutputPluginBase(TopInOutPluginBase, ABC):
|
||||
)
|
||||
|
||||
@abstractmethod
|
||||
def dumpbytes(
|
||||
self, data: "SingleTrack", config: Optional[PluginConfig]
|
||||
) -> BinaryIO:
|
||||
def stream_dump(self, data: "SingleTrack", config) -> Iterator[bytes]:
|
||||
"""将单个音轨导出为对应格式的字节流
|
||||
|
||||
参数
|
||||
@@ -563,15 +567,12 @@ class TrackOutputPluginBase(TopInOutPluginBase, ABC):
|
||||
|
||||
返回
|
||||
====
|
||||
BinaryIO
|
||||
导出后的二进制字节流
|
||||
Iterator[bytes]
|
||||
分块导出的二进制字节串
|
||||
"""
|
||||
pass
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def dump(
|
||||
self, data: "SingleTrack", file_path: Path, config: Optional[PluginConfig]
|
||||
):
|
||||
def dump(self, data: "SingleTrack", file_path: Path, config):
|
||||
"""将单个音轨导出为对应格式的文件
|
||||
|
||||
参数
|
||||
@@ -583,7 +584,9 @@ class TrackOutputPluginBase(TopInOutPluginBase, ABC):
|
||||
config: Optional[PluginConfig]
|
||||
插件配置;**可选**
|
||||
"""
|
||||
pass
|
||||
with file_path.open("wb") as f:
|
||||
for _bytes in self.stream_dump(data, config):
|
||||
f.write(_bytes)
|
||||
|
||||
|
||||
class ServicePluginBase(TopPluginBase, ABC):
|
||||
@@ -601,17 +604,15 @@ class ServicePluginBase(TopPluginBase, ABC):
|
||||
)
|
||||
|
||||
@abstractmethod
|
||||
def serve(self, config: Optional[PluginConfig], *args) -> None:
|
||||
def serve(self, config=None) -> None:
|
||||
"""服务插件的运行逻辑
|
||||
|
||||
参数
|
||||
====
|
||||
config: Optional[PluginConfig]
|
||||
插件配置;**可选**
|
||||
*args: Any
|
||||
其他运行时参数
|
||||
"""
|
||||
pass
|
||||
...
|
||||
|
||||
|
||||
class LibraryPluginBase(TopPluginBase, ABC):
|
||||
@@ -629,4 +630,4 @@ class LibraryPluginBase(TopPluginBase, ABC):
|
||||
)
|
||||
|
||||
# 怎么?
|
||||
# 插件的彼此依赖就不需要什么调用了吧
|
||||
# 插件的依赖项就不需要什么调用了吧
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
音·创 v3 的功能性内容合辑
|
||||
此处存放无需本体类的内容
|
||||
"""
|
||||
|
||||
"""
|
||||
版权所有 © 2026 金羿、玉衡Alioth
|
||||
Copyright © 2026 Eilles, YuhengAlioth
|
||||
|
||||
开源相关声明请见 仓库根目录下的 License.md
|
||||
Terms & Conditions: License.md in the root directory
|
||||
"""
|
||||
|
||||
# 睿乐组织 开发交流群 861684859
|
||||
# Email TriM-Organization@hotmail.com
|
||||
# 若需转载或借鉴 许可声明请查看仓库目录下的 License.md
|
||||
|
||||
|
||||
from copy import deepcopy, copy
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Generator, List, Optional, Tuple, Union, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
def enumerated_stuffcopy_dictionary(
|
||||
enumeration_times: int = 17, staff: T = {}
|
||||
) -> Dict[int, T]:
|
||||
"""
|
||||
生成一个字典,其中键从 `0` 到 `enumeration_times-1`,值是 `staff` 的拷贝
|
||||
"""
|
||||
# 这告诉我们,你不能忽略任何一个复制的序列,因为它真的,我哭死,折磨我一整天,全在这个bug上了
|
||||
# 上面的这指的是 copy.deepcopy —— 金羿 来自 20260210
|
||||
return {i: deepcopy(staff) for i in range(enumeration_times)}
|
||||
|
||||
|
||||
def incremental_save_path(
|
||||
file_name: str,
|
||||
suffix: str = ".file",
|
||||
appendix_connector: str = "-",
|
||||
parent_path: Union[Path, str] = Path.cwd(),
|
||||
) -> Path:
|
||||
"""
|
||||
生成一个文件路径,该文件地址指向的文件名应为 `file_name` 添加一个后缀 `suffix`。\n
|
||||
如果 `file_name` 已经存在,则添加一个后缀 `appendix_connector` 和一个数字。
|
||||
此数字由 2 开始,每存在此文件,则递增 1。直到找到最后那个惟一的未被占用的文件路径。\n
|
||||
例如:
|
||||
- file_name = "test", suffix = ".txt", appendix_connector = "-", parent_path = "/tmp/" \n
|
||||
若在 `/tmp` 中:
|
||||
1. `test.txt` 不存在,则返回 Path("/tmp/test.txt")
|
||||
2. 上述文件存在,且 `test-1.txt` 不存在,则返回 Path("/tmp/test-1.txt")
|
||||
3. 上述文件皆存在,但 `test-2.txt` 不存在,则返回 Path("/tmp/test-2.txt")
|
||||
...
|
||||
"""
|
||||
ss_path = parent_path / Path(file_name).with_suffix(suffix)
|
||||
if ss_path.exists():
|
||||
now_appendix = 2
|
||||
while (
|
||||
ss_path := parent_path
|
||||
/ Path(file_name + appendix_connector + str(now_appendix) + suffix)
|
||||
).exists():
|
||||
now_appendix += 1
|
||||
return ss_path
|
||||
|
||||
|
||||
def volume_weight_to_sound_distance(
|
||||
max_weight: float, minimal_loadness: int, weight: float, loadness: int
|
||||
) -> float:
|
||||
"""
|
||||
通过音量权计算声源距离
|
||||
|
||||
Parameters
|
||||
==========
|
||||
max_weight: float
|
||||
全曲最大音量权
|
||||
minimal_loadness: int
|
||||
曲目中响度最小乐器的响度
|
||||
weight: float
|
||||
当前乐器的音量权
|
||||
loadness: int
|
||||
当前乐器的响度
|
||||
|
||||
Returns
|
||||
=======
|
||||
float
|
||||
声源距离
|
||||
|
||||
Notes
|
||||
=====
|
||||
|
||||
以下关于距离的计算逻辑请见[计算过程](./resources/响度公式/计算过程.jpg)\n
|
||||
关于响度的计算方法请见[此脚本](./resources/experiments/exam_loadness.py)\n
|
||||
最终公式为:\n
|
||||
声源距离 = 48 - (48 * 音量权 * ( 10 ** ((当前乐器的响度 - 响度最小乐器的响度) / 20 ))) / 全曲最大音量权\n
|
||||
D_t = 48 - \\frac{48 \\cdot P_t \\cdot 10^{\\frac{V_t - V_x}{20}}}{P_m}
|
||||
由于我们存储的响度都是 百-LUFS,所以下面的计算除数为 2000
|
||||
"""
|
||||
|
||||
if weight == 0:
|
||||
return 48
|
||||
elif weight == max_weight:
|
||||
# print("音量权最大,差比:", ((loadness - minimal_loadness) / 2000), ",率:", (1 - pow(10, (minimal_loadness - loadness)/2000)))
|
||||
return 48 * (1 - pow(10, (minimal_loadness - loadness) / 2000))
|
||||
else:
|
||||
return 48 * (
|
||||
1 - pow(10, (minimal_loadness - loadness) / 2000) * weight / max_weight
|
||||
)
|
||||
@@ -0,0 +1,61 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
音·创 v3 内置的乐曲查看器
|
||||
"""
|
||||
|
||||
"""
|
||||
版权所有 © 2026 金羿
|
||||
Copyright © 2026 Eilles
|
||||
|
||||
开源相关声明请见 仓库根目录下的 License.md
|
||||
Terms & Conditions: License.md in the root directory
|
||||
"""
|
||||
|
||||
# 睿乐组织 开发交流群 861684859
|
||||
# Email TriM-Organization@hotmail.com
|
||||
# 若需转载或借鉴 许可声明请查看仓库目录下的 License.md
|
||||
|
||||
from io import BytesIO
|
||||
from typing import BinaryIO, Optional, Iterator, Generator, Any, Tuple
|
||||
from pathlib import Path
|
||||
from dataclasses import dataclass
|
||||
|
||||
from TrimMCStruct import Structure
|
||||
|
||||
from Musicreater import SingleMusic, SingleTrack
|
||||
from Musicreater.plugins import (
|
||||
PluginConfig,
|
||||
PluginMetaInformation,
|
||||
PluginTypes,
|
||||
MusicOperatePluginBase,
|
||||
music_operate_plugin,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TerminalSeekerConfig(PluginConfig):
|
||||
"""
|
||||
终端查看器配置
|
||||
"""
|
||||
|
||||
|
||||
|
||||
@music_operate_plugin("music_terminal_seeker")
|
||||
class TerminalSeekerPlugin(MusicOperatePluginBase):
|
||||
|
||||
metainfo = PluginMetaInformation(
|
||||
name="基于终端的音乐查看器",
|
||||
author="金羿",
|
||||
description="将乐曲信息输出到终端",
|
||||
version=(0,0,1),
|
||||
type=PluginTypes.FUNCTION_MUSIC_OPERATE,
|
||||
license="Same as Musiccreater",
|
||||
dependencies=(),
|
||||
)
|
||||
|
||||
def process(
|
||||
self, data: "SingleMusic", config: TerminalSeekerConfig
|
||||
) -> "SingleMusic":
|
||||
...
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
音·创 v3 内置的 Minecraft 指令结构生成插件
|
||||
"""
|
||||
|
||||
"""
|
||||
版权所有 © 2026 金羿、玉衡Alioth
|
||||
Copyright © 2026 Eilles, YuhengAlioth
|
||||
|
||||
开源相关声明请见 仓库根目录下的 License.md
|
||||
Terms & Conditions: License.md in the root directory
|
||||
"""
|
||||
|
||||
# 睿乐组织 开发交流群 861684859
|
||||
# Email TriM-Organization@hotmail.com
|
||||
# 若需转载或借鉴 许可声明请查看仓库目录下的 License.md
|
||||
|
||||
from .main import McstructureExportConfig, NoteDataConvert2CommandPlugin
|
||||
|
||||
|
||||
__all__ = [
|
||||
"McstructureExportConfig",
|
||||
"NoteDataConvert2CommandPlugin",
|
||||
]
|
||||
@@ -0,0 +1,103 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
音·创 v3 内置的 Minecraft 结构生成插件中有关附加包操作的内容
|
||||
"""
|
||||
|
||||
"""
|
||||
版权所有 © 2026 金羿、玉衡Alioth
|
||||
Copyright © 2026 Eilles, YuhengAlioth
|
||||
|
||||
开源相关声明请见 仓库根目录下的 License.md
|
||||
Terms & Conditions: License.md in the root directory
|
||||
"""
|
||||
|
||||
# 睿乐组织 开发交流群 861684859
|
||||
# Email TriM-Organization@hotmail.com
|
||||
# 若需转载或借鉴 许可声明请查看仓库目录下的 License.md
|
||||
|
||||
|
||||
import datetime
|
||||
import os
|
||||
import uuid
|
||||
import zipfile
|
||||
from typing import List, Literal, Union
|
||||
|
||||
|
||||
def compress_zipfile(sourceDir, outFilename, compression=8, exceptFile=None):
|
||||
"""
|
||||
使用指定的压缩算法将目录打包为zip文件
|
||||
|
||||
Parameters
|
||||
------------
|
||||
sourceDir: str
|
||||
要压缩的源目录路径
|
||||
outFilename: str
|
||||
输出的zip文件路径
|
||||
compression: int, 可选
|
||||
压缩算法,默认为8 (DEFLATED)
|
||||
可用算法:
|
||||
STORED = 0
|
||||
DEFLATED = 8 (默认)
|
||||
BZIP2 = 12
|
||||
LZMA = 14
|
||||
exceptFile: list[str], 可选
|
||||
需要排除在压缩包外的文件名称列表(可选)
|
||||
|
||||
Returns
|
||||
---------
|
||||
None
|
||||
"""
|
||||
|
||||
zipf = zipfile.ZipFile(outFilename, "w", compression)
|
||||
pre_len = len(os.path.dirname(sourceDir))
|
||||
for parent, dirnames, filenames in os.walk(sourceDir):
|
||||
for filename in filenames:
|
||||
if filename == exceptFile:
|
||||
continue
|
||||
pathfile = os.path.join(parent, filename)
|
||||
arc_name = pathfile[pre_len:].strip(os.path.sep) # 相对路径
|
||||
zipf.write(pathfile, arc_name)
|
||||
zipf.close()
|
||||
|
||||
|
||||
def behavior_mcpack_manifest(
|
||||
format_version: Union[Literal[1], Literal[2]] = 1,
|
||||
pack_description: str = "",
|
||||
pack_version: Union[List[int], Literal[None]] = None,
|
||||
pack_name: str = "",
|
||||
pack_uuid: Union[str, Literal[None]] = None,
|
||||
pack_engine_version: Union[List[int], None] = None,
|
||||
modules_description: str = "",
|
||||
modules_version: List[int] = [0, 0, 1],
|
||||
modules_uuid: Union[str, Literal[None]] = None,
|
||||
):
|
||||
"""
|
||||
生成一个我的世界行为包组件的定义清单文件
|
||||
"""
|
||||
if not pack_version:
|
||||
now_date = datetime.datetime.now()
|
||||
pack_version = [
|
||||
now_date.year,
|
||||
now_date.month * 100 + now_date.day,
|
||||
now_date.hour * 100 + now_date.minute,
|
||||
]
|
||||
result = {
|
||||
"format_version": format_version,
|
||||
"header": {
|
||||
"description": pack_description,
|
||||
"version": pack_version,
|
||||
"name": pack_name,
|
||||
"uuid": str(uuid.uuid4()) if not pack_uuid else pack_uuid,
|
||||
},
|
||||
"modules": [
|
||||
{
|
||||
"description": modules_description,
|
||||
"type": "data",
|
||||
"version": modules_version,
|
||||
"uuid": str(uuid.uuid4()) if not modules_uuid else modules_uuid,
|
||||
}
|
||||
],
|
||||
}
|
||||
if pack_engine_version:
|
||||
result["header"]["min_engine_version"] = pack_engine_version
|
||||
return result
|
||||
+8
-8
@@ -1,11 +1,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
存放有关BDX结构操作的内容
|
||||
音·创 v3 内置的 Minecraft 结构生成插件中有关 BDX 结构操作的内容
|
||||
"""
|
||||
|
||||
"""
|
||||
版权所有 © 2025 金羿 & 诸葛亮与八卦阵
|
||||
Copyright © 2025 Eilles & bgArray
|
||||
版权所有 © 2026 金羿、玉衡Alioth
|
||||
Copyright © 2026 Eilles, YuhengAlioth
|
||||
|
||||
开源相关声明请见 仓库根目录下的 License.md
|
||||
Terms & Conditions: License.md in the root directory
|
||||
@@ -15,11 +15,11 @@ Terms & Conditions: License.md in the root directory
|
||||
# Email TriM-Organization@hotmail.com
|
||||
# 若需转载或借鉴 许可声明请查看仓库目录下的 License.md
|
||||
|
||||
|
||||
from typing import List
|
||||
|
||||
from ..constants import x, y, z
|
||||
from ..subclass import MineCommand
|
||||
from .common import bottem_side_length_of_smallest_square_bottom_box
|
||||
from Musicreater.builtin_plugins.to_commands import MineCommand
|
||||
from .common import bottem_side_length_of_smallest_square_bottom_box, x, y, z
|
||||
|
||||
BDX_MOVE_KEY = {
|
||||
"x": [b"\x0f", b"\x0e", b"\x1c", b"\x14", b"\x15"],
|
||||
@@ -161,7 +161,7 @@ def commands_to_BDX_bytes(
|
||||
|
||||
for command in commands_list:
|
||||
_bytes += form_command_block_in_BDX_bytes(
|
||||
command.command_text,
|
||||
command.command,
|
||||
(
|
||||
(1 if y_forward else 0)
|
||||
if (
|
||||
@@ -181,7 +181,7 @@ def commands_to_BDX_bytes(
|
||||
condition=command.conditional,
|
||||
needRedstone=False,
|
||||
tickDelay=command.delay,
|
||||
customName=command.annotation_text,
|
||||
customName=command.annotation,
|
||||
executeOnFirstTick=False,
|
||||
trackOutput=True,
|
||||
)
|
||||
+6
-2
@@ -4,8 +4,8 @@
|
||||
"""
|
||||
|
||||
"""
|
||||
版权所有 © 2025 金羿 & 诸葛亮与八卦阵
|
||||
Copyright © 2025 Eilles & bgArray
|
||||
版权所有 © 2026 金羿、玉衡Alioth
|
||||
Copyright © 2026 Eilles, YuhengAlioth
|
||||
|
||||
开源相关声明请见 仓库根目录下的 License.md
|
||||
Terms & Conditions: License.md in the root directory
|
||||
@@ -18,6 +18,10 @@ Terms & Conditions: License.md in the root directory
|
||||
|
||||
import math
|
||||
|
||||
x = "x"
|
||||
y = "y"
|
||||
z = "z"
|
||||
|
||||
|
||||
def bottem_side_length_of_smallest_square_bottom_box(
|
||||
_total_block_count: int, _max_height: int
|
||||
@@ -0,0 +1,148 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
音·创 v3 内置的 Minecraft 结构生成插件
|
||||
"""
|
||||
|
||||
"""
|
||||
版权所有 © 2026 金羿、玉衡Alioth
|
||||
Copyright © 2026 Eilles, YuhengAlioth
|
||||
|
||||
开源相关声明请见 仓库根目录下的 License.md
|
||||
Terms & Conditions: License.md in the root directory
|
||||
"""
|
||||
|
||||
# 睿乐组织 开发交流群 861684859
|
||||
# Email TriM-Organization@hotmail.com
|
||||
# 若需转载或借鉴 许可声明请查看仓库目录下的 License.md
|
||||
|
||||
from io import BytesIO
|
||||
from typing import BinaryIO, Optional, Iterator, Generator, Any, Tuple
|
||||
from pathlib import Path
|
||||
from dataclasses import dataclass
|
||||
|
||||
from TrimMCStruct import Structure
|
||||
|
||||
from Musicreater import SingleMusic, SingleTrack
|
||||
from Musicreater.plugins import (
|
||||
PluginConfig,
|
||||
PluginMetaInformation,
|
||||
PluginTypes,
|
||||
music_output_plugin,
|
||||
MusicOutputPluginBase,
|
||||
track_output_plugin,
|
||||
TrackOutputPluginBase,
|
||||
)
|
||||
|
||||
from Musicreater.builtin_plugins.to_commands import (
|
||||
MineCommand,
|
||||
NoteDataConvert2CommandPlugin,
|
||||
)
|
||||
|
||||
from .mcstructure import (
|
||||
COMPABILITY_VERSION_117,
|
||||
COMPABILITY_VERSION_119,
|
||||
commands_to_structure,
|
||||
commands_to_redstone_delay_structure,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class McstructureExportConfig(PluginConfig):
|
||||
"""
|
||||
导出 MCSTRUCTURE 结构插件的配置类
|
||||
"""
|
||||
|
||||
music_deviation: float = 0
|
||||
"""
|
||||
全曲音调偏移调整,单位为 Midi Pitch
|
||||
"""
|
||||
minimum_volume: float = 0.01
|
||||
"""
|
||||
指令参数:最小音量
|
||||
"""
|
||||
player_selector: str = "@a"
|
||||
"""
|
||||
玩家选择器
|
||||
"""
|
||||
max_height: int = 64
|
||||
"""
|
||||
生成结构的最大高度
|
||||
"""
|
||||
enable_old_execute_format: bool = False
|
||||
"""
|
||||
是否使用旧版指令格式
|
||||
"""
|
||||
|
||||
@property
|
||||
def execute_command_head(self) -> str:
|
||||
return (
|
||||
"execute {} ~ ~ ~ "
|
||||
if self.enable_old_execute_format
|
||||
else "execute as {} at @s positioned ~ ~ ~ run "
|
||||
)
|
||||
|
||||
|
||||
@music_output_plugin("music_to_mcstructure_in_delay_plugin")
|
||||
class MusicExportToMcstructureWithDelayPlayerPlugin(MusicOutputPluginBase):
|
||||
|
||||
metainfo = PluginMetaInformation(
|
||||
name="导出全曲至结构(mcstructure结构、延迟播放器)",
|
||||
author="金羿、玉衡Alioth",
|
||||
description="将音·创 v3 的整首音乐数据,以指令方块延迟的播放形式,导出为基岩版 MCSTRUCTURE 结构",
|
||||
version=(0, 0, 1),
|
||||
type=PluginTypes.FUNCTION_MUSIC_EXPORT,
|
||||
license="Same as Musicreater",
|
||||
dependencies=("notedata_to_command_plugin",),
|
||||
)
|
||||
|
||||
supported_formats = ("MCSTRUCTURE",)
|
||||
|
||||
@staticmethod
|
||||
def _go_convertion(
|
||||
data: SingleMusic, config: McstructureExportConfig
|
||||
) -> Tuple[Structure, Tuple[int, int, int], int]:
|
||||
|
||||
command_list, max_delay = (
|
||||
NoteDataConvert2CommandPlugin.to_command_list_in_delay(
|
||||
music=data,
|
||||
music_deviation=config.music_deviation,
|
||||
minimum_volume=config.minimum_volume,
|
||||
player_selector=config.player_selector,
|
||||
execute_command_head=config.execute_command_head,
|
||||
)[:2]
|
||||
)
|
||||
|
||||
struct, size, end_pos = commands_to_structure(
|
||||
command_list,
|
||||
config.max_height - 1,
|
||||
compability_version_=(
|
||||
COMPABILITY_VERSION_117
|
||||
if config.enable_old_execute_format
|
||||
else COMPABILITY_VERSION_119
|
||||
),
|
||||
)
|
||||
|
||||
return struct, size, max_delay
|
||||
|
||||
def dump(self, data: SingleMusic, file_path: Path, config: McstructureExportConfig):
|
||||
|
||||
struct, size, max_delay = self._go_convertion(data, config)
|
||||
|
||||
file_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with file_path.open("wb") as f:
|
||||
struct.dump(f)
|
||||
|
||||
# return size, max_delay
|
||||
|
||||
def stream_dump(
|
||||
self, data: SingleMusic, config: McstructureExportConfig
|
||||
) -> Iterator[bytes]:
|
||||
struct, size, max_delay = self._go_convertion(data, config)
|
||||
|
||||
b_out = BytesIO()
|
||||
struct.dump(b_out)
|
||||
b_out.seek(0)
|
||||
|
||||
yield from b_out
|
||||
+26
-17
@@ -1,11 +1,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
存放有关MCSTRUCTURE结构操作的内容
|
||||
音·创 v3 内置的 Minecraft 结构生成插件中有关 MCSTRUCTURE 结构操作的内容
|
||||
"""
|
||||
|
||||
"""
|
||||
版权所有 © 2025 金羿 & 诸葛亮与八卦阵
|
||||
Copyright © 2025 Eilles & bgArray
|
||||
版权所有 © 2026 金羿、玉衡Alioth
|
||||
Copyright © 2026 Eilles, YuhengAlioth
|
||||
|
||||
开源相关声明请见 仓库根目录下的 License.md
|
||||
Terms & Conditions: License.md in the root directory
|
||||
@@ -20,16 +20,22 @@ from typing import List, Literal, Tuple
|
||||
|
||||
from TrimMCStruct import Block, Structure, TAG_Byte, TAG_Long
|
||||
|
||||
from ..constants import x, y, z
|
||||
from ..subclass import MineCommand
|
||||
from .common import bottem_side_length_of_smallest_square_bottom_box
|
||||
from Musicreater.builtin_plugins.to_commands import MineCommand
|
||||
from .common import bottem_side_length_of_smallest_square_bottom_box, x, y, z
|
||||
|
||||
|
||||
def antiaxis(axis: Literal["x", "z", "X", "Z"]):
|
||||
"""
|
||||
在 x-z 平面上,返回指定轴的另一个轴
|
||||
"""
|
||||
|
||||
return z if axis == x else x
|
||||
|
||||
|
||||
def forward_IER(forward: bool):
|
||||
"""
|
||||
把用逻辑值标记的方向值缓存正负数,用以乘上增量
|
||||
"""
|
||||
return 1 if forward else -1
|
||||
|
||||
|
||||
@@ -47,6 +53,9 @@ AXIS_PARTICULAR_VALUE = {
|
||||
False: 2,
|
||||
},
|
||||
}
|
||||
"""
|
||||
指令方块朝向对应特殊值
|
||||
"""
|
||||
|
||||
# 1.19的结构兼容版本号
|
||||
COMPABILITY_VERSION_119: int = 17959425
|
||||
@@ -279,12 +288,12 @@ def commands_to_structure(
|
||||
结构类, 结构占用大小, 终点坐标
|
||||
"""
|
||||
|
||||
_sideLength = bottem_side_length_of_smallest_square_bottom_box(
|
||||
_side_length = bottem_side_length_of_smallest_square_bottom_box(
|
||||
len(commands), max_height
|
||||
)
|
||||
|
||||
struct = Structure(
|
||||
size=(_sideLength, max_height, _sideLength), # 声明结构大小
|
||||
size=(_side_length, max_height, _side_length), # 声明结构大小
|
||||
compability_version=compability_version_,
|
||||
)
|
||||
|
||||
@@ -300,7 +309,7 @@ def commands_to_structure(
|
||||
struct.set_block(
|
||||
coordinate,
|
||||
form_command_block_in_NBT_struct(
|
||||
command=command.command_text,
|
||||
command=command.command,
|
||||
coordinate=coordinate,
|
||||
particularValue=(
|
||||
(1 if y_forward else 0)
|
||||
@@ -312,7 +321,7 @@ def commands_to_structure(
|
||||
(3 if z_forward else 2)
|
||||
if (
|
||||
((now_z != 0) and (not z_forward))
|
||||
or (z_forward and (now_z != _sideLength - 1))
|
||||
or (z_forward and (now_z != _side_length - 1))
|
||||
)
|
||||
else 5
|
||||
)
|
||||
@@ -321,7 +330,7 @@ def commands_to_structure(
|
||||
condition=False,
|
||||
alwaysRun=True,
|
||||
tickDelay=command.delay,
|
||||
customName=command.annotation_text,
|
||||
customName=command.annotation,
|
||||
executeOnFirstTick=False,
|
||||
trackOutput=True,
|
||||
compability_version_number=compability_version_,
|
||||
@@ -337,7 +346,7 @@ def commands_to_structure(
|
||||
|
||||
now_z += 1 if z_forward else -1
|
||||
|
||||
if ((now_z >= _sideLength) and z_forward) or (
|
||||
if ((now_z >= _side_length) and z_forward) or (
|
||||
(now_z < 0) and (not z_forward)
|
||||
):
|
||||
now_z -= 1 if z_forward else -1
|
||||
@@ -349,7 +358,7 @@ def commands_to_structure(
|
||||
(
|
||||
now_x + 1,
|
||||
max_height if now_x or now_z else now_y,
|
||||
_sideLength if now_x else now_z,
|
||||
_side_length if now_x else now_z,
|
||||
),
|
||||
(now_x, now_y, now_z),
|
||||
)
|
||||
@@ -486,7 +495,7 @@ def commands_to_redstone_delay_structure(
|
||||
struct.set_block(
|
||||
(pos_now[x], 1, pos_now[z]),
|
||||
form_command_block_in_NBT_struct(
|
||||
command=cmd.command_text,
|
||||
command=cmd.command,
|
||||
coordinate=(pos_now[x], 1, pos_now[z]),
|
||||
particularValue=command_statevalue(extensioon_direction, forward),
|
||||
# impluse= (0 if first_impluse else 2),
|
||||
@@ -494,7 +503,7 @@ def commands_to_redstone_delay_structure(
|
||||
condition=False,
|
||||
alwaysRun=False,
|
||||
tickDelay=cmd.delay % 2,
|
||||
customName=cmd.annotation_text,
|
||||
customName=cmd.annotation,
|
||||
compability_version_number=compability_version_,
|
||||
),
|
||||
)
|
||||
@@ -518,7 +527,7 @@ def commands_to_redstone_delay_structure(
|
||||
struct.set_block(
|
||||
(now_pos_copy[x], 1, now_pos_copy[z]),
|
||||
form_command_block_in_NBT_struct(
|
||||
command=cmd.command_text,
|
||||
command=cmd.command,
|
||||
coordinate=(now_pos_copy[x], 1, now_pos_copy[z]),
|
||||
particularValue=command_statevalue(extensioon_direction, forward),
|
||||
# impluse= (0 if first_impluse else 2),
|
||||
@@ -526,7 +535,7 @@ def commands_to_redstone_delay_structure(
|
||||
condition=False,
|
||||
alwaysRun=False,
|
||||
tickDelay=cmd.delay % 2,
|
||||
customName=cmd.annotation_text,
|
||||
customName=cmd.annotation,
|
||||
compability_version_number=compability_version_,
|
||||
),
|
||||
)
|
||||
@@ -1,59 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
音·创 v3 内置的 Midi 读取插件
|
||||
"""
|
||||
|
||||
"""
|
||||
版权所有 © 2026 金羿、玉衡Alioth
|
||||
Copyright © 2026 Eilles, YuhengAlioth
|
||||
|
||||
开源相关声明请见 仓库根目录下的 License.md
|
||||
Terms & Conditions: License.md in the root directory
|
||||
"""
|
||||
|
||||
# 睿乐组织 开发交流群 861684859
|
||||
# Email TriM-Organization@hotmail.com
|
||||
# 若需转载或借鉴 许可声明请查看仓库目录下的 License.md
|
||||
|
||||
import mido
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from typing import BinaryIO, Optional
|
||||
|
||||
from Musicreater import SingleMusic
|
||||
from Musicreater.plugins import (
|
||||
music_input_plugin,
|
||||
PluginConfig,
|
||||
PluginMetaInformation,
|
||||
PluginTypes,
|
||||
MusicInputPluginBase,
|
||||
)
|
||||
|
||||
|
||||
@music_input_plugin("midi_2_music_plugin")
|
||||
class MidiImport2MusicPlugin(MusicInputPluginBase):
|
||||
"""Midi 音乐数据导入插件"""
|
||||
|
||||
metainfo = PluginMetaInformation(
|
||||
name="Midi 导入插件",
|
||||
author="金羿、玉衡Alioth",
|
||||
description="从 Midi 文件导入音乐数据",
|
||||
version=(0, 0, 1),
|
||||
type=PluginTypes.FUNCTION_MUSIC_IMPORT,
|
||||
license="Same as Musicreater",
|
||||
)
|
||||
|
||||
supported_formats = ("MID", "MIDI")
|
||||
|
||||
def loadbytes(
|
||||
self, bytes_buffer_in: BinaryIO, config: PluginConfig | None
|
||||
) -> SingleMusic:
|
||||
midi_file = mido.MidiFile(file=bytes_buffer_in)
|
||||
return SingleMusic() # =========================== TODO: 等待制作
|
||||
|
||||
def load(self, file_path: Path, config: Optional[PluginConfig]) -> "SingleMusic":
|
||||
"""从 Midi 文件导入音乐数据"""
|
||||
midi_file = mido.MidiFile(filename=file_path)
|
||||
return SingleMusic() # =========================== TODO: 等待制作
|
||||
@@ -0,0 +1,57 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
音·创 v3 内置的 Midi 读取插件
|
||||
"""
|
||||
|
||||
"""
|
||||
版权所有 © 2026 金羿、玉衡Alioth、偷吃不是Touch
|
||||
Copyright © 2026 Eilles, YuhengAlioth, Touch
|
||||
|
||||
开源相关声明请见 仓库根目录下的 License.md
|
||||
Terms & Conditions: License.md in the root directory
|
||||
"""
|
||||
|
||||
# 睿乐组织 开发交流群 861684859
|
||||
# Email TriM-Organization@hotmail.com
|
||||
# 若需转载或借鉴 许可声明请查看仓库目录下的 License.md
|
||||
|
||||
|
||||
from .main import MidiImportConfig, MidiImport2MusicPlugin
|
||||
|
||||
|
||||
from .constants import (
|
||||
MIDI_DEFAULT_PROGRAM_VALUE,
|
||||
MIDI_DEFAULT_VOLUME_VALUE,
|
||||
MM_CLASSIC_PITCHED_INSTRUMENT_TABLE,
|
||||
MM_CLASSIC_PERCUSSION_INSTRUMENT_TABLE,
|
||||
MM_DEFAULT_PITCHED_INSTRUMENT_TABLE,
|
||||
MM_DEFAULT_PERCUSSION_INSTRUMENT_TABLE,
|
||||
MM_DISLINK_PITCHED_INSTRUMENT_TABLE,
|
||||
MM_DISLINK_PERCUSSION_INSTRUMENT_TABLE,
|
||||
MM_NBS_PITCHED_INSTRUMENT_TABLE,
|
||||
MM_NBS_PERCUSSION_INSTRUMENT_TABLE,
|
||||
)
|
||||
|
||||
from .utils import panning_2_rotation_linear, panning_2_rotation_trigonometric
|
||||
|
||||
__all__ = [
|
||||
# 插件参数和插件本体类
|
||||
"MidiImportConfig",
|
||||
"MidiImport2MusicPlugin",
|
||||
# 内置的拟合函数
|
||||
"panning_2_rotation_linear",
|
||||
"panning_2_rotation_trigonometric",
|
||||
# Midi 相关默认值
|
||||
"MIDI_DEFAULT_PROGRAM_VALUE",
|
||||
"MIDI_DEFAULT_VOLUME_VALUE",
|
||||
# Midi 与 游戏内容 的对照表
|
||||
"MM_CLASSIC_PITCHED_INSTRUMENT_TABLE",
|
||||
"MM_CLASSIC_PERCUSSION_INSTRUMENT_TABLE",
|
||||
"MM_DEFAULT_PITCHED_INSTRUMENT_TABLE",
|
||||
"MM_DEFAULT_PERCUSSION_INSTRUMENT_TABLE",
|
||||
"MM_DISLINK_PITCHED_INSTRUMENT_TABLE",
|
||||
"MM_DISLINK_PERCUSSION_INSTRUMENT_TABLE",
|
||||
"MM_NBS_PITCHED_INSTRUMENT_TABLE",
|
||||
"MM_NBS_PERCUSSION_INSTRUMENT_TABLE",
|
||||
]
|
||||
@@ -0,0 +1,831 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
音·创 v3 内置的 Midi 读取插件的数值常量
|
||||
"""
|
||||
|
||||
"""
|
||||
版权所有 © 2026 金羿、玉衡Alioth、偷吃不是Touch
|
||||
Copyright © 2026 Eilles, YuhengAlioth, Touch
|
||||
|
||||
开源相关声明请见 仓库根目录下的 License.md
|
||||
Terms & Conditions: License.md in the root directory
|
||||
"""
|
||||
|
||||
# 睿乐组织 开发交流群 861684859
|
||||
# Email TriM-Organization@hotmail.com
|
||||
# 若需转载或借鉴 许可声明请查看仓库目录下的 License.md
|
||||
|
||||
|
||||
from typing import Dict, List, Tuple
|
||||
|
||||
# Midi用对照表
|
||||
|
||||
MIDI_DEFAULT_VOLUME_VALUE: int = (
|
||||
64 # Midi默认音量,当用户未指定时,默认使用折中默认音量
|
||||
)
|
||||
|
||||
MIDI_DEFAULT_PROGRAM_VALUE: int = (
|
||||
74 # 当 Midi 本身与用户皆未指定音色时,默认 Flute 长笛
|
||||
)
|
||||
|
||||
|
||||
# Midi乐器对MC乐器对照表
|
||||
|
||||
# “经典”对照表,由 Chalie Ping “查理平” 和 金羿ELS 提供
|
||||
|
||||
MM_CLASSIC_PITCHED_INSTRUMENT_TABLE: Dict[int, str] = {
|
||||
0: "note.harp",
|
||||
1: "note.harp",
|
||||
2: "note.pling",
|
||||
3: "note.harp",
|
||||
4: "note.pling",
|
||||
5: "note.pling",
|
||||
6: "note.harp",
|
||||
7: "note.harp",
|
||||
8: "note.snare",
|
||||
9: "note.harp",
|
||||
10: "note.didgeridoo",
|
||||
11: "note.harp",
|
||||
12: "note.xylophone",
|
||||
13: "note.chime",
|
||||
14: "note.harp",
|
||||
15: "note.harp",
|
||||
16: "note.bass",
|
||||
17: "note.harp",
|
||||
18: "note.harp",
|
||||
19: "note.harp",
|
||||
20: "note.harp",
|
||||
21: "note.harp",
|
||||
22: "note.harp",
|
||||
23: "note.guitar",
|
||||
24: "note.guitar",
|
||||
25: "note.guitar",
|
||||
26: "note.guitar",
|
||||
27: "note.guitar",
|
||||
28: "note.guitar",
|
||||
29: "note.guitar",
|
||||
30: "note.guitar",
|
||||
31: "note.bass",
|
||||
32: "note.bass",
|
||||
33: "note.bass",
|
||||
34: "note.bass",
|
||||
35: "note.bass",
|
||||
36: "note.bass",
|
||||
37: "note.bass",
|
||||
38: "note.bass",
|
||||
39: "note.bass",
|
||||
40: "note.harp",
|
||||
41: "note.harp",
|
||||
42: "note.harp",
|
||||
43: "note.harp",
|
||||
44: "note.iron_xylophone",
|
||||
45: "note.guitar",
|
||||
46: "note.harp",
|
||||
47: "note.harp",
|
||||
48: "note.guitar",
|
||||
49: "note.guitar",
|
||||
50: "note.bit",
|
||||
51: "note.bit",
|
||||
52: "note.harp",
|
||||
53: "note.harp",
|
||||
54: "note.bit",
|
||||
55: "note.flute",
|
||||
56: "note.flute",
|
||||
57: "note.flute",
|
||||
58: "note.flute",
|
||||
59: "note.flute",
|
||||
60: "note.flute",
|
||||
61: "note.flute",
|
||||
62: "note.flute",
|
||||
63: "note.flute",
|
||||
64: "note.bit",
|
||||
65: "note.bit",
|
||||
66: "note.bit",
|
||||
67: "note.bit",
|
||||
68: "note.flute",
|
||||
69: "note.harp",
|
||||
70: "note.harp",
|
||||
71: "note.flute",
|
||||
72: "note.flute",
|
||||
73: "note.flute",
|
||||
74: "note.harp",
|
||||
75: "note.flute",
|
||||
76: "note.harp",
|
||||
77: "note.harp",
|
||||
78: "note.harp",
|
||||
79: "note.harp",
|
||||
80: "note.bit",
|
||||
81: "note.bit",
|
||||
82: "note.bit",
|
||||
83: "note.bit",
|
||||
84: "note.bit",
|
||||
85: "note.bit",
|
||||
86: "note.bit",
|
||||
87: "note.bit",
|
||||
88: "note.bit",
|
||||
89: "note.bit",
|
||||
90: "note.bit",
|
||||
91: "note.bit",
|
||||
92: "note.bit",
|
||||
93: "note.bit",
|
||||
94: "note.bit",
|
||||
95: "note.bit",
|
||||
96: "note.bit",
|
||||
97: "note.bit",
|
||||
98: "note.bit",
|
||||
99: "note.bit",
|
||||
100: "note.bit",
|
||||
101: "note.bit",
|
||||
102: "note.bit",
|
||||
103: "note.bit",
|
||||
104: "note.harp",
|
||||
105: "note.banjo",
|
||||
106: "note.harp",
|
||||
107: "note.harp",
|
||||
108: "note.harp",
|
||||
109: "note.harp",
|
||||
110: "note.harp",
|
||||
111: "note.guitar",
|
||||
112: "note.harp",
|
||||
113: "note.bell",
|
||||
114: "note.harp",
|
||||
115: "note.cow_bell",
|
||||
116: "note.bd",
|
||||
117: "note.bass",
|
||||
118: "note.bit",
|
||||
119: "note.bd",
|
||||
120: "note.guitar",
|
||||
121: "note.harp",
|
||||
122: "note.harp",
|
||||
123: "note.harp",
|
||||
124: "note.harp",
|
||||
125: "note.hat",
|
||||
126: "note.bd",
|
||||
127: "note.snare",
|
||||
}
|
||||
"""“经典”乐音乐器对照表"""
|
||||
|
||||
MM_CLASSIC_PERCUSSION_INSTRUMENT_TABLE: Dict[int, str] = {
|
||||
34: "note.bd",
|
||||
35: "note.bd",
|
||||
36: "note.hat",
|
||||
37: "note.snare",
|
||||
38: "note.snare",
|
||||
39: "note.snare",
|
||||
40: "note.hat",
|
||||
41: "note.snare",
|
||||
42: "note.hat",
|
||||
43: "note.snare",
|
||||
44: "note.snare",
|
||||
45: "note.bell",
|
||||
46: "note.snare",
|
||||
47: "note.snare",
|
||||
48: "note.bell",
|
||||
49: "note.hat",
|
||||
50: "note.bell",
|
||||
51: "note.bell",
|
||||
52: "note.bell",
|
||||
53: "note.bell",
|
||||
54: "note.bell",
|
||||
55: "note.bell",
|
||||
56: "note.snare",
|
||||
57: "note.hat",
|
||||
58: "note.chime",
|
||||
59: "note.iron_xylophone",
|
||||
60: "note.bd",
|
||||
61: "note.bd",
|
||||
62: "note.xylophone",
|
||||
63: "note.xylophone",
|
||||
64: "note.xylophone",
|
||||
65: "note.hat",
|
||||
66: "note.bell",
|
||||
67: "note.bell",
|
||||
68: "note.hat",
|
||||
69: "note.hat",
|
||||
70: "note.snare",
|
||||
71: "note.flute",
|
||||
72: "note.hat",
|
||||
73: "note.hat",
|
||||
74: "note.xylophone",
|
||||
75: "note.hat",
|
||||
76: "note.hat",
|
||||
77: "note.xylophone",
|
||||
78: "note.xylophone",
|
||||
79: "note.bell",
|
||||
80: "note.bell",
|
||||
}
|
||||
"""“经典”打击乐器对照表"""
|
||||
|
||||
# Touch “偷吃” 高准确率音色对照表
|
||||
|
||||
MM_TOUCH_PITCHED_INSTRUMENT_TABLE: Dict[int, str] = {
|
||||
0: "note.harp",
|
||||
1: "note.harp",
|
||||
2: "note.pling",
|
||||
3: "note.harp",
|
||||
4: "note.pling",
|
||||
5: "note.pling",
|
||||
6: "note.guitar",
|
||||
7: "note.guitar",
|
||||
8: "note.iron_xylophone",
|
||||
9: "note.bell",
|
||||
10: "note.iron_xylophone",
|
||||
11: "note.iron_xylophone",
|
||||
12: "note.iron_xylophone",
|
||||
13: "note.xylophone",
|
||||
14: "note.chime",
|
||||
15: "note.banjo",
|
||||
16: "note.xylophone",
|
||||
17: "note.iron_xylophone",
|
||||
18: "note.flute",
|
||||
19: "note.flute",
|
||||
20: "note.flute",
|
||||
21: "note.flute",
|
||||
22: "note.flute",
|
||||
23: "note.flute",
|
||||
24: "note.guitar",
|
||||
25: "note.guitar",
|
||||
26: "note.guitar",
|
||||
27: "note.guitar",
|
||||
28: "note.guitar",
|
||||
29: "note.guitar",
|
||||
30: "note.guitar",
|
||||
31: "note.bass",
|
||||
32: "note.bass",
|
||||
33: "note.bass",
|
||||
34: "note.bass",
|
||||
35: "note.bass",
|
||||
36: "note.bass",
|
||||
37: "note.bass",
|
||||
38: "note.bass",
|
||||
39: "note.bass",
|
||||
40: "note.flute",
|
||||
41: "note.flute",
|
||||
42: "note.flute",
|
||||
43: "note.bass",
|
||||
44: "note.flute",
|
||||
45: "note.iron_xylophone",
|
||||
46: "note.harp",
|
||||
47: "note.snare",
|
||||
48: "note.flute",
|
||||
49: "note.flute",
|
||||
50: "note.flute",
|
||||
51: "note.flute",
|
||||
52: "note.didgeridoo",
|
||||
53: "note.flute",
|
||||
54: "note.flute",
|
||||
55: "mob.zombie.wood",
|
||||
56: "note.flute",
|
||||
57: "note.flute",
|
||||
58: "note.flute",
|
||||
59: "note.flute",
|
||||
60: "note.flute",
|
||||
61: "note.flute",
|
||||
62: "note.flute",
|
||||
63: "note.flute",
|
||||
64: "note.bit",
|
||||
65: "note.bit",
|
||||
66: "note.bit",
|
||||
67: "note.bit",
|
||||
68: "note.flute",
|
||||
69: "note.bit",
|
||||
70: "note.banjo",
|
||||
71: "note.flute",
|
||||
72: "note.flute",
|
||||
73: "note.flute",
|
||||
74: "note.flute",
|
||||
75: "note.flute",
|
||||
76: "note.iron_xylophone",
|
||||
77: "note.iron_xylophone",
|
||||
78: "note.flute",
|
||||
79: "note.flute",
|
||||
80: "note.bit",
|
||||
81: "note.bit",
|
||||
82: "note.flute",
|
||||
83: "note.flute",
|
||||
84: "note.guitar",
|
||||
85: "note.flute",
|
||||
86: "note.bass",
|
||||
87: "note.bass",
|
||||
88: "note.bit",
|
||||
89: "note.flute",
|
||||
90: "note.bit",
|
||||
91: "note.flute",
|
||||
92: "note.bell",
|
||||
93: "note.guitar",
|
||||
94: "note.flute",
|
||||
95: "note.bit",
|
||||
96: "note.bit",
|
||||
97: "note.flute",
|
||||
98: "note.bell",
|
||||
99: "note.bit",
|
||||
100: "note.bit",
|
||||
101: "note.bit",
|
||||
102: "note.bit",
|
||||
103: "note.bit",
|
||||
104: "note.iron_xylophone",
|
||||
105: "note.banjo",
|
||||
106: "note.harp",
|
||||
107: "note.harp",
|
||||
108: "note.bell",
|
||||
109: "note.flute",
|
||||
110: "note.flute",
|
||||
111: "note.flute",
|
||||
112: "note.bell",
|
||||
113: "note.xylophone",
|
||||
114: "note.flute",
|
||||
115: "note.hat",
|
||||
116: "note.snare",
|
||||
117: "note.snare",
|
||||
118: "note.bd",
|
||||
119: "firework.blast",
|
||||
120: "note.guitar",
|
||||
121: "note.harp",
|
||||
122: "note.harp",
|
||||
123: "note.harp",
|
||||
124: "note.bit",
|
||||
125: "note.hat",
|
||||
126: "firework.twinkle",
|
||||
127: "mob.zombie.wood",
|
||||
}
|
||||
"""“偷吃”乐音乐器对照表"""
|
||||
|
||||
MM_TOUCH_PERCUSSION_INSTRUMENT_TABLE: Dict[int, str] = {
|
||||
34: "note.hat",
|
||||
35: "note.bd",
|
||||
36: "note.bd",
|
||||
37: "note.snare",
|
||||
38: "note.snare",
|
||||
39: "fire.ignite",
|
||||
40: "note.snare",
|
||||
41: "note.hat",
|
||||
42: "note.hat",
|
||||
43: "firework.blast",
|
||||
44: "note.hat",
|
||||
45: "note.snare",
|
||||
46: "note.snare",
|
||||
47: "note.snare",
|
||||
48: "note.bell",
|
||||
49: "note.hat",
|
||||
50: "note.bell",
|
||||
51: "note.bell",
|
||||
52: "note.bell",
|
||||
53: "note.bell",
|
||||
54: "note.bell",
|
||||
55: "note.bell",
|
||||
56: "note.snare",
|
||||
57: "note.hat",
|
||||
58: "note.chime",
|
||||
59: "note.iron_xylophone",
|
||||
60: "note.bd",
|
||||
61: "note.bd",
|
||||
62: "note.xylophone",
|
||||
63: "note.xylophone",
|
||||
64: "note.xylophone",
|
||||
65: "note.hat",
|
||||
66: "note.bell",
|
||||
67: "note.bell",
|
||||
68: "note.hat",
|
||||
69: "note.hat",
|
||||
70: "note.snare",
|
||||
71: "note.flute",
|
||||
72: "note.hat",
|
||||
73: "note.hat",
|
||||
74: "note.xylophone",
|
||||
75: "note.hat",
|
||||
76: "note.hat",
|
||||
77: "note.xylophone",
|
||||
78: "note.xylophone",
|
||||
79: "note.bell",
|
||||
80: "note.bell",
|
||||
}
|
||||
"""“偷吃”打击乐器对照表"""
|
||||
|
||||
|
||||
# 金羿音色对照表,在偷吃的表的基础上修改而来
|
||||
|
||||
MM_EILLES_PITCHED_INSTRUMENT_TABLE: Dict[int, str] = (
|
||||
MM_TOUCH_PITCHED_INSTRUMENT_TABLE
|
||||
| {
|
||||
57: "note.trumpet",
|
||||
58: "note.trumpet_exposed",
|
||||
59: "note.trumpet_weathered",
|
||||
61: "note.trumpet",
|
||||
62: "note.trumpet_oxidized",
|
||||
63: "note.trumpet_weathered",
|
||||
64: "note.trumpet_oxidized",
|
||||
65: "note.trumpet",
|
||||
66: "note.trumpet_exposed",
|
||||
67: "note.trumpet_weathered",
|
||||
68: "note.trumpet_oxidized",
|
||||
69: "note.trumpet",
|
||||
70: "note.trumpet",
|
||||
71: "note.trumpet_exposed",
|
||||
72: "note.banjo",
|
||||
}
|
||||
)
|
||||
"""“金羿”乐音乐器对照表"""
|
||||
MM_EILLES_PERCUSSION_INSTRUMENT_TABLE: Dict[int, str] = {}
|
||||
"""“金羿”打击乐器对照表"""
|
||||
|
||||
|
||||
# Dislink “断联” 音色对照表
|
||||
# https://github.com/Dislink/midi2bdx/blob/main/index.html
|
||||
|
||||
|
||||
MM_DISLINK_PITCHED_INSTRUMENT_TABLE: Dict[int, str] = {
|
||||
0: "note.harp",
|
||||
1: "note.harp",
|
||||
2: "note.pling",
|
||||
3: "note.harp",
|
||||
4: "note.harp",
|
||||
5: "note.harp",
|
||||
6: "note.harp",
|
||||
7: "note.harp",
|
||||
8: "note.iron_xylophone",
|
||||
9: "note.bell",
|
||||
10: "note.iron_xylophone",
|
||||
11: "note.iron_xylophone",
|
||||
12: "note.iron_xylophone",
|
||||
13: "note.iron_xylophone",
|
||||
14: "note.chime",
|
||||
15: "note.iron_xylophone",
|
||||
16: "note.harp",
|
||||
17: "note.harp",
|
||||
18: "note.harp",
|
||||
19: "note.harp",
|
||||
20: "note.harp",
|
||||
21: "note.harp",
|
||||
22: "note.harp",
|
||||
23: "note.harp",
|
||||
24: "note.guitar",
|
||||
25: "note.guitar",
|
||||
26: "note.guitar",
|
||||
27: "note.guitar",
|
||||
28: "note.guitar",
|
||||
29: "note.guitar",
|
||||
30: "note.guitar",
|
||||
31: "note.guitar",
|
||||
32: "note.bass",
|
||||
33: "note.bass",
|
||||
34: "note.bass",
|
||||
35: "note.bass",
|
||||
36: "note.bass",
|
||||
37: "note.bass",
|
||||
38: "note.bass",
|
||||
39: "note.bass",
|
||||
40: "note.harp",
|
||||
41: "note.flute",
|
||||
42: "note.flute",
|
||||
43: "note.flute",
|
||||
44: "note.flute",
|
||||
45: "note.harp",
|
||||
46: "note.harp",
|
||||
47: "note.harp",
|
||||
48: "note.harp",
|
||||
49: "note.harp",
|
||||
50: "note.harp",
|
||||
51: "note.harp",
|
||||
52: "note.harp",
|
||||
53: "note.harp",
|
||||
54: "note.harp",
|
||||
55: "note.harp",
|
||||
56: "note.harp",
|
||||
57: "note.harp",
|
||||
58: "note.harp",
|
||||
59: "note.harp",
|
||||
60: "note.harp",
|
||||
61: "note.harp",
|
||||
62: "note.harp",
|
||||
63: "note.harp",
|
||||
64: "note.harp",
|
||||
65: "note.harp",
|
||||
66: "note.harp",
|
||||
67: "note.harp",
|
||||
68: "note.harp",
|
||||
69: "note.harp",
|
||||
70: "note.harp",
|
||||
71: "note.harp",
|
||||
72: "note.flute",
|
||||
73: "note.flute",
|
||||
74: "note.flute",
|
||||
75: "note.flute",
|
||||
76: "note.flute",
|
||||
77: "note.flute",
|
||||
78: "note.flute",
|
||||
79: "note.flute",
|
||||
80: "note.bit",
|
||||
81: "note.bit",
|
||||
82: "note.harp",
|
||||
83: "note.harp",
|
||||
84: "note.harp",
|
||||
85: "note.harp",
|
||||
86: "note.harp",
|
||||
87: "note.harp",
|
||||
88: "note.harp",
|
||||
89: "note.harp",
|
||||
90: "note.harp",
|
||||
91: "note.harp",
|
||||
92: "note.harp",
|
||||
93: "note.harp",
|
||||
94: "note.harp",
|
||||
95: "note.harp",
|
||||
96: "note.harp",
|
||||
97: "note.harp",
|
||||
98: "note.harp",
|
||||
99: "note.harp",
|
||||
100: "note.harp",
|
||||
101: "note.harp",
|
||||
102: "note.harp",
|
||||
103: "note.harp",
|
||||
104: "note.harp",
|
||||
105: "note.banjo",
|
||||
106: "note.harp",
|
||||
107: "note.harp",
|
||||
108: "note.harp",
|
||||
109: "note.harp",
|
||||
110: "note.harp",
|
||||
111: "note.harp",
|
||||
112: "note.cow_bell",
|
||||
113: "note.harp",
|
||||
114: "note.harp",
|
||||
115: "note.bd",
|
||||
116: "note.bd",
|
||||
117: "note.bd",
|
||||
118: "note.bd",
|
||||
119: "note.harp",
|
||||
120: "note.harp",
|
||||
121: "note.harp",
|
||||
122: "note.harp",
|
||||
123: "note.harp",
|
||||
124: "note.harp",
|
||||
125: "note.harp",
|
||||
126: "note.harp",
|
||||
127: "note.harp",
|
||||
}
|
||||
"""“断联”乐音乐器对照表"""
|
||||
|
||||
MM_DISLINK_PERCUSSION_INSTRUMENT_TABLE: Dict[int, str] = {
|
||||
34: "note.bd",
|
||||
35: "note.bd",
|
||||
36: "note.snare",
|
||||
37: "note.snare",
|
||||
38: "note.bd",
|
||||
39: "note.snare",
|
||||
40: "note.bd",
|
||||
41: "note.hat",
|
||||
42: "note.bd",
|
||||
43: "note.hat",
|
||||
44: "note.bd",
|
||||
45: "note.hat",
|
||||
46: "note.bd",
|
||||
47: "note.bd",
|
||||
48: "note.bd",
|
||||
49: "note.bd",
|
||||
50: "note.bd",
|
||||
51: "note.bd",
|
||||
52: "note.bd",
|
||||
53: "note.bd",
|
||||
54: "note.bd",
|
||||
55: "note.cow_bell",
|
||||
56: "note.bd",
|
||||
57: "note.bd",
|
||||
58: "note.bd",
|
||||
59: "note.bd",
|
||||
60: "note.bd",
|
||||
61: "note.bd",
|
||||
62: "note.bd",
|
||||
63: "note.bd",
|
||||
64: "note.bd",
|
||||
65: "note.bd",
|
||||
66: "note.bd",
|
||||
67: "note.bd",
|
||||
68: "note.bd",
|
||||
69: "note.bd",
|
||||
70: "note.bd",
|
||||
71: "note.bd",
|
||||
72: "note.bd",
|
||||
73: "note.bd",
|
||||
74: "note.bd",
|
||||
75: "note.bd",
|
||||
76: "note.bd",
|
||||
77: "note.bd",
|
||||
78: "note.bd",
|
||||
79: "note.bd",
|
||||
80: "note.bd",
|
||||
}
|
||||
"""“断联”打击乐器对照表"""
|
||||
|
||||
# NoteBlockStudio “NBS”音色对照表
|
||||
# https://github.com/OpenNBS/NoteBlockStudio/blob/main/scripts/midi_instruments/midi_instruments.gml
|
||||
# 此表来自于 Commit 1ab5357c197872495197f27ad8374d711b2a5195
|
||||
# 需要更新:https://github.com/OpenNBS/NoteBlockStudio/compare/main...development?diff=unified&w
|
||||
|
||||
MM_NBS_PITCHED_INSTRUMENT_TABLE: Dict[int, str] = {
|
||||
0: "note.harp",
|
||||
1: "note.pling",
|
||||
2: "note.harp",
|
||||
3: "note.pling",
|
||||
4: "note.harp",
|
||||
5: "note.harp",
|
||||
6: "note.guitar",
|
||||
7: "note.banjo",
|
||||
8: "note.bell",
|
||||
9: "note.bell",
|
||||
10: "note.bell",
|
||||
11: "note.iron_xylophone",
|
||||
12: "note.iron_xylophone",
|
||||
13: "note.xylophone",
|
||||
14: "note.bell",
|
||||
15: "note.iron_xylophone",
|
||||
16: "note.flute",
|
||||
17: "note.flute",
|
||||
18: "note.flute",
|
||||
19: "note.flute",
|
||||
20: "note.flute",
|
||||
21: "note.flute",
|
||||
22: "note.flute",
|
||||
23: "note.flute",
|
||||
24: "note.guitar",
|
||||
25: "note.guitar",
|
||||
26: "note.guitar",
|
||||
27: "note.bass",
|
||||
28: "note.guitar",
|
||||
29: "note.guitar",
|
||||
30: "note.bass",
|
||||
31: "note.bass",
|
||||
32: "note.bass",
|
||||
33: "note.guitar",
|
||||
34: "note.guitar",
|
||||
35: "note.bass",
|
||||
36: "note.pling",
|
||||
37: "note.flute",
|
||||
38: "note.flute",
|
||||
39: "note.flute",
|
||||
40: "note.flute",
|
||||
41: "note.flute",
|
||||
42: "note.didgeridoo",
|
||||
43: "note.flute",
|
||||
44: "note.didgeridoo",
|
||||
45: "note.flute",
|
||||
46: "note.flute",
|
||||
47: "note.flute",
|
||||
48: "note.flute",
|
||||
49: "note.flute",
|
||||
50: "note.flute",
|
||||
51: "note.flute",
|
||||
52: "note.flute",
|
||||
53: "note.flute",
|
||||
54: "note.flute",
|
||||
55: "note.flute",
|
||||
56: "note.flute",
|
||||
57: "note.flute",
|
||||
58: "note.flute",
|
||||
59: "note.flute",
|
||||
60: "note.bit",
|
||||
61: "note.flute",
|
||||
62: "note.flute",
|
||||
63: "note.flute",
|
||||
64: "note.flute",
|
||||
65: "note.guitar",
|
||||
66: "note.flute",
|
||||
67: "note.flute",
|
||||
68: "note.flute",
|
||||
69: "note.bell",
|
||||
70: "note.flute",
|
||||
71: "note.flute",
|
||||
72: "note.flute",
|
||||
73: "note.flute",
|
||||
74: "note.chime",
|
||||
75: "note.flute",
|
||||
76: "note.flute",
|
||||
77: "note.guitar",
|
||||
78: "note.pling",
|
||||
79: "note.flute",
|
||||
80: "note.guitar",
|
||||
81: "note.banjo",
|
||||
82: "note.banjo",
|
||||
83: "note.banjo",
|
||||
84: "note.guitar",
|
||||
85: "note.iron_xylophone",
|
||||
86: "note.flute",
|
||||
87: "note.flute",
|
||||
88: "note.chime",
|
||||
89: "note.cow_bell",
|
||||
90: "note.iron_xylophone",
|
||||
91: "note.xylophone",
|
||||
92: "note.basedrum",
|
||||
93: "note.snare",
|
||||
94: "note.snare",
|
||||
95: "note.basedrum",
|
||||
96: "note.snare",
|
||||
97: "note.hat",
|
||||
98: "note.snare",
|
||||
99: "note.hat",
|
||||
100: "note.basedrum",
|
||||
101: "note.hat",
|
||||
102: "note.basedrum",
|
||||
103: "note.hat",
|
||||
104: "note.basedrum",
|
||||
105: "note.snare",
|
||||
106: "note.snare",
|
||||
107: "note.snare",
|
||||
108: "note.cow_bell",
|
||||
109: "note.snare",
|
||||
110: "note.hat",
|
||||
111: "note.snare",
|
||||
112: "note.hat",
|
||||
113: "note.hat",
|
||||
114: "note.hat",
|
||||
115: "note.hat",
|
||||
116: "note.hat",
|
||||
117: "note.chime",
|
||||
118: "note.hat",
|
||||
119: "note.snare",
|
||||
120: "note.hat",
|
||||
121: "note.hat",
|
||||
122: "note.hat",
|
||||
123: "note.hat",
|
||||
124: "note.hat",
|
||||
125: "note.snare",
|
||||
126: "note.basedrum",
|
||||
127: "note.basedrum",
|
||||
}
|
||||
"""“NBS”乐音乐器对照表"""
|
||||
|
||||
|
||||
MM_NBS_PERCUSSION_INSTRUMENT_TABLE: Dict[int, str] = {
|
||||
24: "note.bit",
|
||||
25: "note.snare",
|
||||
26: "note.hat",
|
||||
27: "note.snare",
|
||||
28: "note.snare",
|
||||
29: "note.hat",
|
||||
30: "note.hat",
|
||||
31: "note.hat",
|
||||
32: "note.hat",
|
||||
33: "note.hat",
|
||||
34: "note.chime",
|
||||
35: "note.basedrum",
|
||||
36: "note.basedrum",
|
||||
37: "note.hat",
|
||||
38: "note.snare",
|
||||
39: "note.hat",
|
||||
40: "note.snare",
|
||||
41: "note.basedrum",
|
||||
42: "note.snare",
|
||||
43: "note.basedrum",
|
||||
44: "note.snare",
|
||||
45: "note.basedrum",
|
||||
46: "note.basedrum",
|
||||
47: "note.snare",
|
||||
48: "note.snare",
|
||||
49: "note.snare",
|
||||
50: "note.snare",
|
||||
51: "note.snare",
|
||||
52: "note.snare",
|
||||
53: "note.hat",
|
||||
54: "note.snare",
|
||||
55: "note.snare",
|
||||
56: "note.cow_bell",
|
||||
57: "note.snare",
|
||||
58: "note.hat",
|
||||
59: "note.snare",
|
||||
60: "note.hat",
|
||||
61: "note.hat",
|
||||
62: "note.hat",
|
||||
63: "note.basedrum",
|
||||
64: "note.basedrum",
|
||||
65: "note.snare",
|
||||
66: "note.snare",
|
||||
67: "note.xylophone",
|
||||
68: "note.xylophone",
|
||||
69: "note.hat",
|
||||
70: "note.hat",
|
||||
71: "note.flute",
|
||||
72: "note.flute",
|
||||
73: "note.hat",
|
||||
74: "note.hat",
|
||||
75: "note.hat",
|
||||
76: "note.hat",
|
||||
77: "note.hat",
|
||||
78: "note.didgeridoo",
|
||||
79: "note.didgeridoo",
|
||||
80: "note.hat",
|
||||
81: "note.chime",
|
||||
82: "note.hat",
|
||||
83: "note.chime",
|
||||
84: "note.chime",
|
||||
85: "note.hat",
|
||||
86: "note.basedrum",
|
||||
87: "note.basedrum",
|
||||
}
|
||||
"""“NBS”打击乐器对照表"""
|
||||
|
||||
|
||||
# 默认音色对照表
|
||||
MM_DEFAULT_PITCHED_INSTRUMENT_TABLE = MM_EILLES_PITCHED_INSTRUMENT_TABLE
|
||||
"""默认的乐音乐器音色对照表"""
|
||||
MM_DEFAULT_PERCUSSION_INSTRUMENT_TABLE = MM_TOUCH_PERCUSSION_INSTRUMENT_TABLE
|
||||
"""默认的打击乐器音色对照表"""
|
||||
@@ -0,0 +1,69 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
音·创 v3 内置的 Midi 读取插件用到的一些报错类型
|
||||
"""
|
||||
|
||||
"""
|
||||
版权所有 © 2026 金羿 & 玉衡Alioth
|
||||
Copyright © 2026 Eilles & YuhengAlioth
|
||||
|
||||
开源相关声明请见 仓库根目录下的 License.md
|
||||
Terms & Conditions: License.md in the root directory
|
||||
"""
|
||||
|
||||
# 睿乐组织 开发交流群 861684859
|
||||
# Email TriM-Organization@hotmail.com
|
||||
# 若需转载或借鉴 许可声明请查看仓库目录下的 License.md
|
||||
|
||||
|
||||
|
||||
from Musicreater.exceptions import MusicreaterOuterlyError
|
||||
|
||||
|
||||
class MidiFormatError(MusicreaterOuterlyError):
|
||||
"""音·创 的所有MIDI格式错误均继承于此"""
|
||||
|
||||
def __init__(self, *args):
|
||||
"""音·创 的所有MIDI格式错误均继承于此"""
|
||||
super().__init__("MIDI 格式错误 - ", *args)
|
||||
|
||||
|
||||
class NotDefineTempoError(MidiFormatError):
|
||||
"""没有Tempo设定导致时间无法计算的错误"""
|
||||
|
||||
def __init__(self, *args):
|
||||
"""没有Tempo设定导致时间无法计算的错误"""
|
||||
super().__init__("在曲目开始时没有声明 Tempo(未指定拍长):", *args)
|
||||
|
||||
|
||||
class ChannelOverFlowError(MidiFormatError):
|
||||
"""一个midi中含有过多的通道"""
|
||||
|
||||
def __init__(self, max_channel=16, *args):
|
||||
"""一个midi中含有过多的通道"""
|
||||
super().__init__("含有过多的通道(数量应≤{}):".format(max_channel), *args)
|
||||
|
||||
|
||||
class NotDefineProgramError(MidiFormatError):
|
||||
"""没有Program设定导致没有乐器可以选择的错误"""
|
||||
|
||||
def __init__(self, *args):
|
||||
"""没有Program设定导致没有乐器可以选择的错误"""
|
||||
super().__init__("未指定演奏乐器:", *args)
|
||||
|
||||
|
||||
class NoteOnOffMismatchError(MidiFormatError):
|
||||
"""音符开音和停止不匹配的错误"""
|
||||
|
||||
def __init__(self, *args):
|
||||
"""音符开音和停止不匹配的错误"""
|
||||
super().__init__("音符不匹配:", *args)
|
||||
|
||||
|
||||
class LyricMismatchError(MidiFormatError):
|
||||
"""歌词匹配解析错误"""
|
||||
|
||||
def __init__(self, *args):
|
||||
"""有可能产生了错误的歌词解析"""
|
||||
super().__init__("歌词解析错误:", *args)
|
||||
@@ -0,0 +1,625 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
音·创 v3 内置的 Midi 读取插件
|
||||
"""
|
||||
|
||||
"""
|
||||
版权所有 © 2026 金羿、玉衡Alioth、偷吃不是Touch
|
||||
Copyright © 2026 Eilles, YuhengAlioth, Touch
|
||||
|
||||
开源相关声明请见 仓库根目录下的 License.md
|
||||
Terms & Conditions: License.md in the root directory
|
||||
"""
|
||||
|
||||
# 睿乐组织 开发交流群 861684859
|
||||
# Email TriM-Organization@hotmail.com
|
||||
# 若需转载或借鉴 许可声明请查看仓库目录下的 License.md
|
||||
|
||||
import mido
|
||||
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import BinaryIO, Optional, Dict, List, Callable, Tuple, Mapping
|
||||
|
||||
from Musicreater import (
|
||||
SingleMusic,
|
||||
SingleTrack,
|
||||
SingleNote,
|
||||
SoundAtmos,
|
||||
CurvableParam,
|
||||
ParamCurve,
|
||||
Instrument,
|
||||
)
|
||||
from Musicreater.plugins import (
|
||||
music_input_plugin,
|
||||
PluginConfig,
|
||||
PluginMetaInformation,
|
||||
PluginTypes,
|
||||
MusicInputPluginBase,
|
||||
)
|
||||
from Musicreater.constants import SoundInformation, MC_INSTRUMENT_SOUND_INFO_TABLE
|
||||
from Musicreater.exceptions import ZeroSpeedError, IllegalMinimumVolumeError
|
||||
from Musicreater.paramcurve import Keyframe as ParamKeyframe
|
||||
from Musicreater._utils import enumerated_stuffcopy_dictionary
|
||||
|
||||
from .constants import (
|
||||
MIDI_DEFAULT_PROGRAM_VALUE,
|
||||
MIDI_DEFAULT_VOLUME_VALUE,
|
||||
MM_DEFAULT_PERCUSSION_INSTRUMENT_TABLE,
|
||||
MM_DEFAULT_PITCHED_INSTRUMENT_TABLE,
|
||||
)
|
||||
from .exceptions import (
|
||||
NoteOnOffMismatchError,
|
||||
ChannelOverFlowError,
|
||||
LyricMismatchError,
|
||||
)
|
||||
from .utils import (
|
||||
panning_2_rotation_trigonometric,
|
||||
midi_msgs_to_noteinfo,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class MidiImportConfig(PluginConfig):
|
||||
"""Midi 音乐数据导入插件配置"""
|
||||
|
||||
# 系统设置
|
||||
ignore_errors: bool = True
|
||||
|
||||
# 处理设置
|
||||
speed_multiplier: float = 1.0
|
||||
|
||||
# 兼容不良 Midi 所定义的默认值
|
||||
default_program_value: int = MIDI_DEFAULT_PROGRAM_VALUE
|
||||
default_volume_value: int = MIDI_DEFAULT_VOLUME_VALUE
|
||||
default_tempo_value: int = mido.midifiles.midifiles.DEFAULT_TEMPO
|
||||
midi_charset: str = "latin1"
|
||||
string_encoding: str = "utf-8" # 默认歌词编码
|
||||
"""
|
||||
默认字串编码,设定为空时不对原 Midi 中的字符串二次编码
|
||||
"""
|
||||
|
||||
# 对照表,此处 None 值在下边 post init 函数中有处理
|
||||
pitched_note_reference_table: Mapping[int, str] = None # type: ignore
|
||||
percussion_note_reference_table: Mapping[int, str] = None # type: ignore
|
||||
note_replacement_table: Mapping[str, str] = None # type: ignore
|
||||
instrument_information_table: Mapping[str, SoundInformation] = None # type: ignore
|
||||
|
||||
# 参数转换相关
|
||||
keep_velocity_to_param_curve: bool = True
|
||||
panning_processing_function: Callable[[float], float] = (
|
||||
panning_2_rotation_trigonometric
|
||||
)
|
||||
|
||||
# 分轨方式
|
||||
divide_tracks_by_miditrack: bool = True
|
||||
divide_tracks_by_midichannel: bool = False
|
||||
divide_tracks_by_soundname: bool = True
|
||||
divide_tracks_by_volume: bool = False
|
||||
divide_tracks_by_panning: bool = False
|
||||
|
||||
def __post_init__(self):
|
||||
self.pitched_note_reference_table = (
|
||||
self.pitched_note_reference_table
|
||||
if self.pitched_note_reference_table
|
||||
else MM_DEFAULT_PITCHED_INSTRUMENT_TABLE
|
||||
)
|
||||
self.percussion_note_reference_table = (
|
||||
self.percussion_note_reference_table
|
||||
if self.percussion_note_reference_table
|
||||
else MM_DEFAULT_PERCUSSION_INSTRUMENT_TABLE
|
||||
)
|
||||
self.note_replacement_table = (
|
||||
self.note_replacement_table if self.note_replacement_table else {}
|
||||
)
|
||||
self.instrument_information_table = (
|
||||
self.instrument_information_table
|
||||
if self.instrument_information_table
|
||||
else MC_INSTRUMENT_SOUND_INFO_TABLE
|
||||
)
|
||||
|
||||
|
||||
class ControlerKeys(Enum):
|
||||
"""
|
||||
Midi 控制器键
|
||||
"""
|
||||
|
||||
MIDI_PROGRAM = "midi_program"
|
||||
MIDI_VOLUME = "midi_volume"
|
||||
MIDI_PAN = "midi_pan"
|
||||
|
||||
|
||||
class TrackDivisionDict(
|
||||
Dict[
|
||||
Tuple[
|
||||
Optional[int],
|
||||
Optional[int],
|
||||
Optional[str],
|
||||
Optional[float],
|
||||
Optional[Tuple[float, float]],
|
||||
],
|
||||
SingleTrack,
|
||||
]
|
||||
):
|
||||
"""
|
||||
音轨分轨字典
|
||||
键为音轨信息元组[音轨编号, 通道编号, 乐器名称, 音量, 声相]
|
||||
值为音轨对象
|
||||
"""
|
||||
|
||||
division_by_miditrack: bool = True
|
||||
division_by_midichannel: bool = False
|
||||
division_by_soundname: bool = True
|
||||
division_by_volume: bool = False
|
||||
division_by_panning: bool = False
|
||||
|
||||
create_volume_curve: bool = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*args,
|
||||
midi_import_config: MidiImportConfig = MidiImportConfig(),
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.division_by_miditrack = midi_import_config.divide_tracks_by_miditrack
|
||||
self.division_by_midichannel = midi_import_config.divide_tracks_by_midichannel
|
||||
self.division_by_soundname = midi_import_config.divide_tracks_by_soundname
|
||||
self.division_by_volume = midi_import_config.divide_tracks_by_volume
|
||||
self.division_by_panning = midi_import_config.divide_tracks_by_panning
|
||||
|
||||
self.create_volume_curve = midi_import_config.keep_velocity_to_param_curve
|
||||
|
||||
def __getitem__(
|
||||
self,
|
||||
key: Tuple[
|
||||
Optional[int],
|
||||
Optional[int],
|
||||
Optional[str],
|
||||
Optional[float],
|
||||
Optional[Tuple[float, float]],
|
||||
],
|
||||
) -> SingleTrack:
|
||||
key = (
|
||||
key[0] if self.division_by_miditrack else None,
|
||||
key[1] if self.division_by_midichannel else None,
|
||||
key[2] if self.division_by_soundname else None,
|
||||
key[3] if self.division_by_volume else None,
|
||||
key[4] if self.division_by_panning else None,
|
||||
)
|
||||
try:
|
||||
return super().__getitem__(key)
|
||||
except KeyError:
|
||||
self[key] = SingleTrack(precise_time=True)
|
||||
if self.create_volume_curve:
|
||||
self[key].argument_curves[CurvableParam.VOLUME] = ParamCurve(
|
||||
base_value=1.0, value_min=0.0, value_max=1.0
|
||||
)
|
||||
return self[key]
|
||||
|
||||
|
||||
@music_input_plugin("midi_to_music_plugin")
|
||||
class MidiImport2MusicPlugin(MusicInputPluginBase):
|
||||
"""Midi 音乐数据导入插件"""
|
||||
|
||||
metainfo = PluginMetaInformation(
|
||||
name="从 Midi 导入全曲",
|
||||
author="金羿、玉衡Alioth",
|
||||
description="从 Midi 文件导入音乐数据",
|
||||
version=(0, 0, 1),
|
||||
type=PluginTypes.FUNCTION_MUSIC_IMPORT,
|
||||
license="Same as Musicreater",
|
||||
)
|
||||
|
||||
supported_formats = ("MID", "MIDI")
|
||||
|
||||
def loadbytes(
|
||||
self,
|
||||
bytes_buffer_in: BinaryIO,
|
||||
config: Optional[MidiImportConfig] = None,
|
||||
) -> SingleMusic:
|
||||
if config is None:
|
||||
config = MidiImportConfig()
|
||||
return self.midifile_2_singlemusic(
|
||||
mido.MidiFile(file=bytes_buffer_in, clip=True, charset=config.midi_charset),
|
||||
config,
|
||||
)
|
||||
|
||||
def load(
|
||||
self, file_path: Path, config: Optional[MidiImportConfig] = None
|
||||
) -> SingleMusic:
|
||||
"""从 Midi 文件导入音乐数据"""
|
||||
if config is None:
|
||||
config = MidiImportConfig()
|
||||
return self.midifile_2_singlemusic(
|
||||
mido.MidiFile(filename=file_path, clip=True, charset=config.midi_charset),
|
||||
config,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def midifile_2_singlemusic(
|
||||
midi: mido.MidiFile,
|
||||
config: MidiImportConfig = MidiImportConfig(),
|
||||
) -> SingleMusic:
|
||||
"""
|
||||
将midi解析并转换为频道音符字典
|
||||
|
||||
Parameters
|
||||
----------
|
||||
midi: mido.MidiFile 对象
|
||||
需要处理的midi对象
|
||||
speed: float
|
||||
音乐播放速度倍数
|
||||
default_program_value: int
|
||||
默认的 MIDI 乐器值
|
||||
default_volume_value: int
|
||||
默认的通道音量值
|
||||
default_tempo_value: int
|
||||
默认的 MIDI TEMPO 值
|
||||
pitched_note_rtable: Dict[int, Tuple[str, int]]
|
||||
乐音乐器Midi-MC对照表
|
||||
percussion_note_rtable: Dict[int, Tuple[str, int]]
|
||||
打击乐器Midi-MC对照表
|
||||
vol_processing_function: Callable[[float], float]
|
||||
音量对播放距离的拟合函数
|
||||
pan_processing_function: Callable[[float], float]
|
||||
声像偏移对播放旋转角度的拟合函数
|
||||
note_rtable_replacement: Dict[str, str]
|
||||
音符名称替换表,此表用于对 Minecraft 乐器名称进行替换,而非 Midi Program 的替换
|
||||
|
||||
Returns
|
||||
-------
|
||||
Tuple[SingleMusic, int, Dict[str, int]]
|
||||
以通道作为分割的Midi音符列表字典, 音符总数, 乐器使用统计
|
||||
"""
|
||||
|
||||
if config.speed_multiplier == 0:
|
||||
raise ZeroSpeedError("播放速度不得为零,应为 (0,1] 范围内的实数。")
|
||||
|
||||
# 一个midi中仅有16个通道 我们通过通道来识别而不是音轨
|
||||
divided_tracks: TrackDivisionDict = TrackDivisionDict(midi_import_config=config)
|
||||
|
||||
value_controler_per_channel: Dict[int, Dict[ControlerKeys, int]] = (
|
||||
enumerated_stuffcopy_dictionary(
|
||||
staff={
|
||||
ControlerKeys.MIDI_PROGRAM: config.default_program_value,
|
||||
ControlerKeys.MIDI_VOLUME: config.default_volume_value,
|
||||
ControlerKeys.MIDI_PAN: 64,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
note_count: int = 0
|
||||
"""音符计数"""
|
||||
note_count_per_instrument: Dict[str, int] = {}
|
||||
"""乐器使用统计"""
|
||||
master_volume: int = 16383
|
||||
"""主音量"""
|
||||
music_name: str = ""
|
||||
"""音乐名称"""
|
||||
|
||||
note_queue_A: Dict[int, List[Tuple[int, int]]] = (
|
||||
enumerated_stuffcopy_dictionary(staff=[])
|
||||
)
|
||||
"""音符队列甲 Dict[通道, List[Tuple[int音高, int轨道]]]"""
|
||||
note_queue_B: Dict[int, List[Tuple[int, int, int, int, int]]] = (
|
||||
enumerated_stuffcopy_dictionary(staff=[])
|
||||
)
|
||||
"""音符队列乙 Dict[通道, List[Tuple[int力度, int乐器, int音量, int偏移, int微秒时间]]]"""
|
||||
|
||||
midi_lyric_cache: List[Tuple[int, str]] = []
|
||||
"""歌词缓存 List[Tuple[int微秒时间, str歌词内容]]"""
|
||||
|
||||
midi_text_list: List[str] = []
|
||||
"""Midi 附加文本列表"""
|
||||
midi_copyright_list: List[str] = []
|
||||
"""Midi 版权列表"""
|
||||
midi_track_name_dict: Dict[int, str] = {}
|
||||
"""轨道名称字典 Dict[int轨道编号, str轨道名称]"""
|
||||
track_0 = midi.tracks[0].copy()
|
||||
should_insert_controls = len(midi.tracks) > 1
|
||||
|
||||
for track_no, message_track in enumerate(midi.tracks):
|
||||
# 每个音轨单独重置
|
||||
|
||||
if track_no == 0 and should_insert_controls:
|
||||
# 第零个音轨在多轨模式下只是作为全局的控制轨出现的,因此忽略
|
||||
# 但是,以防这里出现了不符合的情况,我们做如下控制
|
||||
trk = []
|
||||
tk0 = []
|
||||
for msg in mido.midifiles.tracks._to_abstime(track_0):
|
||||
if msg.type == "track_name":
|
||||
music_name = (
|
||||
msg.name.encode("latin1").decode(config.string_encoding)
|
||||
if config.string_encoding
|
||||
else msg.name
|
||||
)
|
||||
elif (msg.type == "set_tempo") or (msg.type == "sysex"):
|
||||
tk0.append(msg)
|
||||
else:
|
||||
trk.append(msg)
|
||||
|
||||
track_0 = mido.MidiTrack(
|
||||
mido.midifiles.tracks.fix_end_of_track(
|
||||
mido.midifiles.tracks._to_reltime(
|
||||
sorted(tk0, key=lambda a: a.time), skip_checks=True
|
||||
),
|
||||
skip_checks=True,
|
||||
)
|
||||
)
|
||||
|
||||
midi.tracks.insert(
|
||||
1,
|
||||
mido.MidiTrack(
|
||||
mido.midifiles.tracks.fix_end_of_track(
|
||||
mido.midifiles.tracks._to_reltime(
|
||||
sorted(trk, key=lambda a: a.time), skip_checks=True
|
||||
),
|
||||
skip_checks=True,
|
||||
)
|
||||
),
|
||||
)
|
||||
del tk0, trk
|
||||
continue
|
||||
|
||||
# print("正在处理轨道:", track_no, "需要合并:", should_insert_controls, "合并长度:", len(track_0))
|
||||
|
||||
midi_tempo: int = config.default_tempo_value
|
||||
"""微秒每拍"""
|
||||
microseconds: int = 0
|
||||
"""当前的微妙时间"""
|
||||
|
||||
for msg in (
|
||||
mido.merge_tracks((track_0, message_track), skip_checks=True)
|
||||
if should_insert_controls
|
||||
else message_track
|
||||
):
|
||||
|
||||
if msg.time != 0:
|
||||
# 微秒
|
||||
# 通常情况下,tempo 是 500000,tpb 在
|
||||
microseconds += msg.time * midi_tempo / midi.ticks_per_beat
|
||||
|
||||
if msg.type == "set_tempo":
|
||||
# Tempo 改变是一个全局的控制
|
||||
# 而且应该是很早出现的一个 Midi 消息
|
||||
midi_tempo = msg.tempo
|
||||
# print(
|
||||
# "Tempo 改变:",
|
||||
# midi_tempo,
|
||||
# "出现在轨道:",
|
||||
# track_no,
|
||||
# "时间:",
|
||||
# microseconds,
|
||||
# flush=True,
|
||||
# )
|
||||
elif msg.type == "sysex":
|
||||
# 系统执行消息
|
||||
data = msg.data # 不包含 F0 和 F7
|
||||
# data: (7F, device_id, 04, 01, LSB, MSB)
|
||||
if (
|
||||
len(data) >= 6
|
||||
and data[0] == 0x7F
|
||||
and data[2] == 0x04
|
||||
and data[3] == 0x01
|
||||
):
|
||||
# 检查 Master Volume 消息
|
||||
master_volume = (data[5] << 7) | data[
|
||||
4
|
||||
] # 14-bit value: 0 ~ 16383
|
||||
elif msg.type == "program_change":
|
||||
# 检测 乐器变化 之 midi 事件
|
||||
value_controler_per_channel[msg.channel][
|
||||
ControlerKeys.MIDI_PROGRAM
|
||||
] = msg.program
|
||||
|
||||
elif msg.is_cc(7):
|
||||
# Control Change 更改当前通道的 音量 的事件(大幅度,最高有效位)
|
||||
# print("通道、轨道、音量修改:",msg.channel, track_no, msg.value)
|
||||
value_controler_per_channel[msg.channel][
|
||||
ControlerKeys.MIDI_VOLUME
|
||||
] = msg.value
|
||||
elif msg.is_cc(10):
|
||||
# Control Change 更改当前通道的 音调偏移 的事件(大幅度,最高有效位)
|
||||
value_controler_per_channel[msg.channel][
|
||||
ControlerKeys.MIDI_PAN
|
||||
] = msg.value
|
||||
|
||||
elif msg.type == "lyrics":
|
||||
# 歌词事件
|
||||
midi_lyric_cache.append((microseconds, msg.text))
|
||||
# print(lyric_cache, flush=True)
|
||||
elif msg.type == "text":
|
||||
# 检测文本事件
|
||||
midi_text_list.append(msg.text)
|
||||
elif msg.type == "copyright":
|
||||
# 检测版权事件
|
||||
midi_copyright_list.append(msg.text)
|
||||
elif msg.type == "track_name":
|
||||
# 检测轨道名称事件
|
||||
midi_track_name_dict[track_no] = (
|
||||
msg.name.encode("latin1").decode(config.string_encoding)
|
||||
if config.string_encoding
|
||||
else msg.name
|
||||
)
|
||||
elif msg.type == "note_on" and msg.velocity != 0:
|
||||
# 一个音符开始弹奏
|
||||
|
||||
# 加入音符队列甲(按通道分隔)
|
||||
# (音高, 轨道)
|
||||
note_queue_A[msg.channel].append((msg.note, track_no))
|
||||
# 音符队列乙(按通道分隔)
|
||||
# (力度, 乐器, 音量, 偏移, 微秒)
|
||||
note_queue_B[msg.channel].append(
|
||||
(
|
||||
msg.velocity,
|
||||
value_controler_per_channel[msg.channel][
|
||||
ControlerKeys.MIDI_PROGRAM
|
||||
],
|
||||
value_controler_per_channel[msg.channel][
|
||||
ControlerKeys.MIDI_VOLUME
|
||||
],
|
||||
value_controler_per_channel[msg.channel][
|
||||
ControlerKeys.MIDI_PAN
|
||||
],
|
||||
microseconds,
|
||||
)
|
||||
)
|
||||
|
||||
elif (msg.type == "note_off") or (
|
||||
msg.type == "note_on" and msg.velocity == 0
|
||||
):
|
||||
# 一个音符结束弹奏
|
||||
|
||||
if (
|
||||
msg.note,
|
||||
track_no,
|
||||
) in note_queue_A[msg.channel]:
|
||||
# 在甲队列中发现了同一个 音高和乐器且在同轨道 的音符
|
||||
|
||||
# 获取其音符力度和微秒数
|
||||
_velocity, _program, _volume, _panning, _start_ms = (
|
||||
note_queue_B[msg.channel][
|
||||
note_queue_A[msg.channel].index((msg.note, track_no))
|
||||
]
|
||||
)
|
||||
|
||||
# 在队列中删除此音符
|
||||
note_queue_A[msg.channel].remove((msg.note, track_no))
|
||||
note_queue_B[msg.channel].remove(
|
||||
(_velocity, _program, _volume, _panning, _start_ms)
|
||||
)
|
||||
|
||||
_lyric = ""
|
||||
# 找一找歌词吧
|
||||
if midi_lyric_cache:
|
||||
for i in range(len(midi_lyric_cache)):
|
||||
if midi_lyric_cache[i][0] >= _start_ms:
|
||||
_lyric = midi_lyric_cache.pop(i)[1]
|
||||
break
|
||||
|
||||
# 更新结果信息
|
||||
|
||||
that_note, sound_name, sound_rotation = midi_msgs_to_noteinfo(
|
||||
inst=(
|
||||
msg.note
|
||||
if (_is_percussion := (msg.channel == 9))
|
||||
else _program
|
||||
),
|
||||
note=(_program if _is_percussion else msg.note),
|
||||
percussive=_is_percussion,
|
||||
volume=_volume,
|
||||
master_volume=master_volume,
|
||||
velocity=_velocity,
|
||||
panning=_panning,
|
||||
start_time=_start_ms, # 微秒
|
||||
duration=microseconds - _start_ms, # 微秒
|
||||
play_speed=config.speed_multiplier,
|
||||
midi_reference_table=(
|
||||
config.percussion_note_reference_table
|
||||
if _is_percussion
|
||||
else config.pitched_note_reference_table
|
||||
),
|
||||
panning_processing_method=config.panning_processing_function,
|
||||
note_table_replacement=config.note_replacement_table,
|
||||
lyric_line=(
|
||||
_lyric.encode("latin1").decode(config.string_encoding)
|
||||
if config.string_encoding
|
||||
else _lyric
|
||||
),
|
||||
)
|
||||
|
||||
# print(that_note.start_time, end=", ")
|
||||
|
||||
now_track = divided_tracks[
|
||||
(
|
||||
track_no,
|
||||
msg.channel,
|
||||
sound_name,
|
||||
_volume * master_volume // 16383,
|
||||
sound_rotation,
|
||||
)
|
||||
]
|
||||
now_track.add(that_note)
|
||||
if config.keep_velocity_to_param_curve:
|
||||
now_track.argument_curves[CurvableParam.VOLUME].add_key( # type: ignore
|
||||
time=that_note.precise_start_time,
|
||||
value=_velocity / 127,
|
||||
)
|
||||
# print("VOLUME:", _velocity / 127, flush=True, end=", ")
|
||||
|
||||
# 更新统计信息
|
||||
note_count += 1
|
||||
if sound_name in note_count_per_instrument.keys():
|
||||
note_count_per_instrument[sound_name] += 1
|
||||
else:
|
||||
note_count_per_instrument[sound_name] = 1
|
||||
|
||||
else:
|
||||
# 什么?找不到 note on 消息??
|
||||
if config.ignore_errors:
|
||||
print(
|
||||
"[WARRING] MIDI格式错误 音符不匹配`{}`无法在上文`{}`中找到与之匹配的音符开音消息".format(
|
||||
msg, note_queue_A[msg.channel]
|
||||
),
|
||||
flush=True,
|
||||
)
|
||||
else:
|
||||
raise NoteOnOffMismatchError(
|
||||
"当前的MIDI很可能有损坏之嫌……",
|
||||
msg,
|
||||
"无法在上文中找到与之匹配的音符开音消息。",
|
||||
)
|
||||
|
||||
if midi_lyric_cache:
|
||||
# 怎么有歌词多啊
|
||||
if config.ignore_errors:
|
||||
print(
|
||||
"[WARRING] MIDI 解析错误 歌词对应错误,以下歌词未能填入音符之中,已经填入的仍可能有误 {}".format(
|
||||
midi_lyric_cache
|
||||
),
|
||||
flush=True,
|
||||
)
|
||||
else:
|
||||
raise LyricMismatchError(
|
||||
"MIDI 解析产生错误",
|
||||
"歌词解析过程中无法对应音符,已填入的音符仍可能有误",
|
||||
midi_lyric_cache,
|
||||
)
|
||||
|
||||
final_music = SingleMusic(
|
||||
name=music_name,
|
||||
credits="; ".join(midi_copyright_list),
|
||||
extra_information={
|
||||
"MIDI_TEXT_LIST": midi_text_list,
|
||||
"NOTE_COUNT": note_count,
|
||||
"NOTE_COUNT_PER_INSTRUMENT": note_count_per_instrument,
|
||||
},
|
||||
)
|
||||
for track_properties, every_single_track in divided_tracks.items():
|
||||
# [音轨编号, 通道编号, 乐器名称, 音量, 声相]
|
||||
if track_properties[0] and (
|
||||
track_name := midi_track_name_dict.get(track_properties[0], "无名音轨")
|
||||
): # 音轨编号
|
||||
every_single_track.name = track_name or "空名称"
|
||||
if track_properties[2]: # 乐器名称
|
||||
every_single_track.instrument = Instrument(
|
||||
track_properties[2],
|
||||
config.instrument_information_table[track_properties[2]]["C-LUFS"],
|
||||
)
|
||||
if track_properties[3]: # 音量权
|
||||
# print(
|
||||
# "[INFO] 音轨 {} 音量权已设置为 {}".format(
|
||||
# every_single_track.name, track_properties[3]
|
||||
# ),
|
||||
# flush=True,
|
||||
# )
|
||||
every_single_track._volume_weight = track_properties[3]
|
||||
if track_properties[4]: # 声相
|
||||
every_single_track.sound_position = track_properties[4]
|
||||
super(SingleMusic, final_music).append(every_single_track)
|
||||
final_music._calc_values()
|
||||
# print("所有声源距离表:",{i:t.sound_distance for i, t in enumerate(final_music)})
|
||||
return final_music
|
||||
@@ -0,0 +1,178 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
音·创 v3 内置的 Midi 读取插件的功能方法
|
||||
"""
|
||||
|
||||
"""
|
||||
版权所有 © 2026 金羿、玉衡Alioth
|
||||
Copyright © 2026 Eilles, YuhengAlioth
|
||||
|
||||
开源相关声明请见 仓库根目录下的 License.md
|
||||
Terms & Conditions: License.md in the root directory
|
||||
"""
|
||||
|
||||
# 睿乐组织 开发交流群 861684859
|
||||
# Email TriM-Organization@hotmail.com
|
||||
# 若需转载或借鉴 许可声明请查看仓库目录下的 License.md
|
||||
|
||||
|
||||
import math
|
||||
|
||||
from typing import Callable, Dict, List, Optional, Sequence, Tuple, Mapping
|
||||
|
||||
from Musicreater import SingleNote, SoundAtmos
|
||||
|
||||
|
||||
|
||||
def panning_2_rotation_linear(pan_: float) -> float:
|
||||
"""
|
||||
Midi 左右平衡偏移值线性转为声源旋转角度
|
||||
|
||||
Parameters
|
||||
----------
|
||||
pan_: int
|
||||
Midi 左右平衡偏移值
|
||||
注:此参数为int,范围从 0 到 127,当为 64 时,声源居中
|
||||
|
||||
Returns
|
||||
-------
|
||||
float
|
||||
声源旋转角度
|
||||
"""
|
||||
return (pan_ - 64) * 90 / 63
|
||||
|
||||
|
||||
def panning_2_rotation_trigonometric(pan_: float) -> float:
|
||||
"""
|
||||
Midi 左右平衡偏移值,依照圆的声场定位,转为声源旋转角度
|
||||
|
||||
Parameters
|
||||
----------
|
||||
pan_: int
|
||||
Midi 左右平衡偏移值
|
||||
注:此参数为int,范围从 0 到 127,当为 64 时,声源居中
|
||||
|
||||
Returns
|
||||
-------
|
||||
float
|
||||
声源旋转角度
|
||||
"""
|
||||
if pan_ <= 0:
|
||||
return -90
|
||||
elif pan_ >= 127:
|
||||
return 90
|
||||
else:
|
||||
return math.degrees(math.acos((64 - pan_) / 63)) - 90
|
||||
|
||||
|
||||
def midi_inst_to_mc_sound(
|
||||
instrumentID: int,
|
||||
reference_table: Mapping[int, str],
|
||||
default_instrument: str = "note.flute",
|
||||
) -> str:
|
||||
"""
|
||||
返回midi的乐器ID对应的我的世界乐器名
|
||||
|
||||
Parameters
|
||||
----------
|
||||
instrumentID: int
|
||||
midi的乐器ID
|
||||
reference_table: Dict[int, Tuple[str, int]]
|
||||
转换乐器参照表
|
||||
default_instrument: str
|
||||
查无此乐器时的替换乐器
|
||||
|
||||
Returns
|
||||
-------
|
||||
str我的世界乐器名
|
||||
"""
|
||||
return reference_table.get(
|
||||
instrumentID,
|
||||
default_instrument,
|
||||
)
|
||||
|
||||
|
||||
def midi_msgs_to_noteinfo(
|
||||
inst: int, # 乐器编号
|
||||
note: int,
|
||||
percussive: bool, # 是否作为打击乐器启用
|
||||
volume: int,
|
||||
master_volume: int,
|
||||
velocity: int,
|
||||
panning: int,
|
||||
start_time: int,
|
||||
duration: int,
|
||||
play_speed: float,
|
||||
midi_reference_table: Mapping[int, str],
|
||||
panning_processing_method: Callable[[float], float],
|
||||
note_table_replacement: Mapping[str, str] = {},
|
||||
lyric_line: str = "",
|
||||
) -> Tuple[SingleNote, str, Tuple[float, float]]:
|
||||
"""
|
||||
将 Midi信息转为音符对象
|
||||
|
||||
Parameters
|
||||
------------
|
||||
inst: int
|
||||
乐器编号
|
||||
note: int
|
||||
音高编号(音符编号)
|
||||
percussive: bool
|
||||
是否作为打击乐器启用
|
||||
volume: int
|
||||
音量
|
||||
master_volume: int
|
||||
主音量
|
||||
velocity: int
|
||||
力度
|
||||
panning: int
|
||||
声相偏移
|
||||
start_time: int
|
||||
音符起始时间(微秒)
|
||||
duration: int
|
||||
音符持续时间(微秒)
|
||||
play_speed: float
|
||||
曲目播放速度
|
||||
midi_reference_table: Dict[int, str]
|
||||
转换对照表
|
||||
panning_processing_method: Callable[[float], float]
|
||||
立体声相偏移处理函数
|
||||
note_table_replacement: Dict[str, str]
|
||||
音符替换表,定义 Minecraft 音符字串的替换
|
||||
lyric_line: str
|
||||
该音符的歌词
|
||||
|
||||
Returns
|
||||
---------
|
||||
Tuple[
|
||||
MineNote我的世界音符对象,
|
||||
str我的世界声音名,
|
||||
Tuple[float, float]声源旋转角度
|
||||
]
|
||||
"""
|
||||
mc_sound_ID = midi_inst_to_mc_sound(
|
||||
inst,
|
||||
midi_reference_table,
|
||||
"note.bd" if percussive else "note.flute",
|
||||
)
|
||||
|
||||
return (
|
||||
SingleNote(
|
||||
note_pitch=note,
|
||||
start_tick=(tk := int(start_time / float(play_speed) / 50000)),
|
||||
keep_tick=round(duration / float(play_speed) / 50000),
|
||||
mass_precision_time=round(
|
||||
(start_time / float(play_speed) - tk * 50000) / 200
|
||||
),
|
||||
extra_information={
|
||||
"LYRIC_TEXT": lyric_line,
|
||||
"VELOCITY_VALUE": velocity,
|
||||
"VOLUME_VALUE": volume,
|
||||
"MASTER_VOLUME_VALUE": master_volume,
|
||||
"PIN_VALUE": panning,
|
||||
},
|
||||
),
|
||||
note_table_replacement.get(mc_sound_ID, mc_sound_ID),
|
||||
(panning_processing_method(panning), 0),
|
||||
)
|
||||
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,159 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
音·创 v3 内置的 音乐预览插件
|
||||
"""
|
||||
|
||||
"""
|
||||
版权所有 © 2026 金羿、鱼旧梦
|
||||
Copyright © 2026 Eilles, ElapsingDreams
|
||||
|
||||
继承自:
|
||||
|
||||
版权所有 © 2026 鱼旧梦
|
||||
Copyright © 2026 ElapsingDreams
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
# 睿乐组织 开发交流群 861684859
|
||||
# Email TriM-Organization@hotmail.com
|
||||
# 本插件依照 Apache 2.0 协议开放源代码,若需转载或借鉴
|
||||
# 许可声明请查看 http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from time import perf_counter_ns
|
||||
from dataclasses import dataclass
|
||||
from typing import BinaryIO, Optional, Iterator, Generator, Any, Tuple, Literal
|
||||
|
||||
|
||||
from Musicreater import SingleMusic, SingleTrack, SingleNote, SoundAtmos, MineNote
|
||||
from Musicreater.plugins import (
|
||||
PluginConfig,
|
||||
PluginMetaInformation,
|
||||
PluginTypes,
|
||||
music_output_plugin,
|
||||
MusicOutputPluginBase,
|
||||
track_output_plugin,
|
||||
TrackOutputPluginBase,
|
||||
)
|
||||
|
||||
|
||||
from .main import MusicPreview
|
||||
|
||||
|
||||
@dataclass
|
||||
class PcmConversionConfig(PluginConfig):
|
||||
|
||||
assets_path: Path
|
||||
"""
|
||||
生成音频文件所使用的资源文件路径
|
||||
"""
|
||||
|
||||
synthesis_mode: Literal[0, 1, 2, 3, 4] = 1
|
||||
"""
|
||||
音频合成模式
|
||||
- 0 原始长度,不变化时长
|
||||
- 1 拉伸至 mc 播放器定义(我的世界效果)
|
||||
- 2 根据 midi 音符长度裁剪
|
||||
- 3 混音预留
|
||||
- 4 匹配 midi 音符长度
|
||||
"""
|
||||
overlay_mode: Literal[1, 2] = 1
|
||||
"""
|
||||
没看懂这个参数的意思
|
||||
"""
|
||||
sample_rate: int = 44100
|
||||
"""
|
||||
目标输出的采样率
|
||||
"""
|
||||
value_get_method: Literal[0, 1] = 1
|
||||
"""
|
||||
采样取值方法,没看懂什么意思
|
||||
- 0 均值化
|
||||
- 1 钳制位
|
||||
"""
|
||||
|
||||
pitch_accuracy_decimals: int = 0
|
||||
"""
|
||||
音调处理精度,小数点后位数
|
||||
"""
|
||||
|
||||
global_volume: float = 1.0
|
||||
"""
|
||||
全局音量控制
|
||||
"""
|
||||
|
||||
global_deviation: float = 0
|
||||
"""
|
||||
全曲音调偏移
|
||||
"""
|
||||
|
||||
|
||||
@music_output_plugin("music_to_pcm_plugin")
|
||||
class NoteDataConvert2PcmPlugin(MusicOutputPluginBase):
|
||||
metainfo = PluginMetaInformation(
|
||||
name="全曲预览播放·PCM",
|
||||
author="金羿、鱼旧梦",
|
||||
description="从全曲的音符数据转换为可以用于播放的 PCM 编码数据",
|
||||
version=(0, 0, 1),
|
||||
type=PluginTypes.FUNCTION_MUSIC_EXPORT,
|
||||
license="Apache 2.0",
|
||||
)
|
||||
|
||||
|
||||
supported_formats = ("WAV", "WAVE", "PCM")
|
||||
|
||||
def dump(self, data: SingleMusic, file_path: Path, config: PcmConversionConfig):
|
||||
|
||||
pr = perf_counter_ns()
|
||||
music_preview = MusicPreview(
|
||||
resource_folder=config.assets_path,
|
||||
synthesis_mode=config.synthesis_mode,
|
||||
overlay_mode=config.overlay_mode,
|
||||
sample_rate=config.sample_rate,
|
||||
value_get_method=config.value_get_method,
|
||||
pitch_accuracy_decimals=config.pitch_accuracy_decimals,
|
||||
music_volume=config.global_volume,
|
||||
music_deviation=config.global_deviation,
|
||||
)
|
||||
music_preview.to_wav_file(data, file_path)
|
||||
af = perf_counter_ns()
|
||||
print("合成用时", af - pr, "纳秒,即", (af - pr) / 1000_000_000, "秒")
|
||||
|
||||
def stream_dump(
|
||||
self, data: SingleMusic, config: PcmConversionConfig
|
||||
) -> Iterator[bytes]:
|
||||
|
||||
pr = perf_counter_ns()
|
||||
music_preview = MusicPreview(
|
||||
resource_folder=config.assets_path,
|
||||
synthesis_mode=config.synthesis_mode,
|
||||
overlay_mode=config.overlay_mode,
|
||||
sample_rate=config.sample_rate,
|
||||
value_get_method=config.value_get_method,
|
||||
pitch_accuracy_decimals=config.pitch_accuracy_decimals,
|
||||
music_volume=config.global_volume,
|
||||
music_deviation=config.global_deviation,
|
||||
)
|
||||
|
||||
b_out = BytesIO()
|
||||
|
||||
music_preview.to_wav_file_byte(data, b_out)
|
||||
af = perf_counter_ns()
|
||||
print("合成用时", af - pr, "纳秒,即", (af - pr) / 1000_000_000, "秒")
|
||||
|
||||
b_out.seek(0)
|
||||
|
||||
yield from b_out
|
||||
@@ -0,0 +1,425 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
音·创 v3 内置的 音乐合成预览功能
|
||||
"""
|
||||
|
||||
"""
|
||||
版权所有 © 2026 金羿、鱼旧梦
|
||||
Copyright © 2026 Eilles, ElapsingDreams
|
||||
|
||||
继承自:
|
||||
|
||||
版权所有 © 2026 鱼旧梦
|
||||
Copyright © 2026 ElapsingDreams
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
# 睿乐组织 开发交流群 861684859
|
||||
# Email TriM-Organization@hotmail.com
|
||||
# 本插件依照 Apache 2.0 协议开放源代码,若需转载或借鉴
|
||||
# 许可声明请查看 http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
|
||||
import os
|
||||
|
||||
from math import inf
|
||||
from typing import BinaryIO, Optional, Iterator, Generator, Any, Tuple, Literal
|
||||
from pathlib import Path
|
||||
|
||||
import soundfile as sf
|
||||
import librosa
|
||||
import numpy as np
|
||||
|
||||
|
||||
from Musicreater import SingleMusic, SingleTrack, SingleNote, SoundAtmos, MineNote
|
||||
from Musicreater.constants import MM_INSTRUMENT_DEVIATION_TABLE
|
||||
|
||||
|
||||
class MusicPreview:
|
||||
|
||||
# 当前的资源存储方法过于鸡肋
|
||||
# 应当依托于 SingleMusic 和 SingleTrack 提供的 extra_info 参数,得到更优的解法
|
||||
# 或者更理想的情况是把当前插件作为一个 Service 插件,然后通过 serve API 进行调用
|
||||
# 但是还没设计好,等未来看看
|
||||
|
||||
"""
|
||||
原始资源存储规范:
|
||||
Dict[str"ID1": Dict[str"SoundID": ndarray[]]]
|
||||
转换资源存储规范:
|
||||
Dict[str"ID1": Dict[str"SoundID": Dict[int"Pitch": ndarray]]]
|
||||
测试阶段,请直接vanilla作为默认传入,不考虑载入
|
||||
"""
|
||||
|
||||
res_raw_data: dict = {} # 原始资源存储
|
||||
# res_raw_index: list = [] # 资源转换情况,key: [88] 留着优化
|
||||
res_data: dict = {} # 转换后资源存储
|
||||
res_data_sr: int # 转换后采样率,和输出采样率一致
|
||||
|
||||
@staticmethod
|
||||
def res_pitch_shift(y: np.ndarray, *, sr: float, n_steps: float, **kwargs: Any):
|
||||
return librosa.effects.pitch_shift(y, sr=sr, n_steps=n_steps)
|
||||
|
||||
@staticmethod
|
||||
def res_time_stretch(y: np.ndarray, *, rate: float, **kwargs: Any):
|
||||
return librosa.effects.time_stretch(y, rate=rate)
|
||||
|
||||
@staticmethod
|
||||
def res_resample(
|
||||
y: np.ndarray,
|
||||
*,
|
||||
orig_sr: float,
|
||||
target_sr: float,
|
||||
fix: bool = True,
|
||||
**kwargs: Any,
|
||||
):
|
||||
return librosa.resample(y, orig_sr=orig_sr, target_sr=target_sr, fix=fix)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
resource_folder: Path,
|
||||
synthesis_mode: Literal[0, 1, 2, 3, 4] = 1,
|
||||
overlay_mode: Literal[1, 2] = 1,
|
||||
sample_rate: int = 44100,
|
||||
value_get_method: Literal[0, 1] = 1,
|
||||
pitch_accuracy_decimals: int = 0,
|
||||
music_volume: float = 1,
|
||||
music_deviation: float = 0,
|
||||
):
|
||||
|
||||
self.mode = synthesis_mode
|
||||
self.overlay_mode_c = overlay_mode
|
||||
self.output_sample_rate = sample_rate
|
||||
self.get_value_method = value_get_method
|
||||
self.resources_path = resource_folder
|
||||
|
||||
# 在当前算法下,小数点精度必须为 0
|
||||
# 此处 TODO,需待旧梦来完善
|
||||
self.pitch_precision_decimals = 0
|
||||
|
||||
self.music_volume = music_volume
|
||||
self.pitch_deviation = music_deviation
|
||||
|
||||
for file in self.resources_path.iterdir():
|
||||
if file.is_file():
|
||||
self.res_raw_data[file.stem] = librosa.load(file, sr=None)
|
||||
|
||||
# print(
|
||||
# file.stem,": { 'AVG':",
|
||||
# np.average(np.absolute(self.res_raw_data[file.stem][0])),", 'MID' :",
|
||||
# np.median(np.absolute(self.res_raw_data[file.stem][0])),", 'MAX' :",
|
||||
# np.max(np.absolute(self.res_raw_data[file.stem][0])),", 'MIN' :",
|
||||
# np.min(np.absolute(self.res_raw_data[file.stem][0])),", 'VAR' :",
|
||||
# np.var(np.absolute(self.res_raw_data[file.stem][0])),"}",
|
||||
# flush=True,
|
||||
# )
|
||||
|
||||
def res_shift(
|
||||
self,
|
||||
sound_name: str,
|
||||
percussive: bool,
|
||||
pitch: int,
|
||||
duration: int,
|
||||
# rate: int
|
||||
):
|
||||
original_y, original_sample_rate = self.res_raw_data[sound_name]
|
||||
if not percussive:
|
||||
if self.mode == 1:
|
||||
# 变调, 时域压扩, 重采样 mc方法
|
||||
self.res_data[sound_name][pitch] = librosa.resample(
|
||||
librosa.effects.time_stretch(
|
||||
librosa.effects.pitch_shift(
|
||||
original_y, sr=original_sample_rate, n_steps=pitch
|
||||
),
|
||||
rate=2 ** (pitch / 12),
|
||||
),
|
||||
orig_sr=original_sample_rate,
|
||||
target_sr=self.output_sample_rate,
|
||||
fix=False,
|
||||
)
|
||||
elif self.mode == 0:
|
||||
# 重采样, 变调
|
||||
self.res_data[sound_name][pitch] = librosa.resample(
|
||||
librosa.effects.pitch_shift(
|
||||
original_y, sr=original_sample_rate, n_steps=pitch
|
||||
),
|
||||
orig_sr=original_sample_rate,
|
||||
target_sr=self.output_sample_rate,
|
||||
fix=False,
|
||||
)
|
||||
elif self.mode == 4:
|
||||
|
||||
# 变调, 时域压扩, 重采样 MIDI-FFT
|
||||
if self.overlay_mode_c == 2:
|
||||
rate = duration / 20 / (len(original_y[0]) / original_sample_rate)
|
||||
else:
|
||||
rate = duration / 20 / (len(original_y) / original_sample_rate)
|
||||
rate = rate if rate != 0 else 1
|
||||
self.res_data[sound_name][pitch] = librosa.resample(
|
||||
librosa.effects.time_stretch(
|
||||
librosa.effects.pitch_shift(
|
||||
original_y, sr=original_sample_rate, n_steps=pitch
|
||||
),
|
||||
rate=rate,
|
||||
),
|
||||
orig_sr=original_sample_rate,
|
||||
target_sr=self.output_sample_rate,
|
||||
fix=False,
|
||||
)
|
||||
elif self.mode == 2:
|
||||
# 变调, 时域压扩, 重采样 MIDI-cut
|
||||
if self.overlay_mode_c == 2:
|
||||
deal = librosa.effects.pitch_shift(
|
||||
original_y, sr=original_sample_rate, n_steps=pitch
|
||||
)[
|
||||
...,
|
||||
: (
|
||||
int(duration / 20 * original_sample_rate)
|
||||
if duration / 20 * original_sample_rate > len(original_y[0])
|
||||
else len(original_y[0])
|
||||
),
|
||||
]
|
||||
else:
|
||||
deal = librosa.effects.pitch_shift(
|
||||
original_y, sr=original_sample_rate, n_steps=pitch
|
||||
)[
|
||||
: (
|
||||
int(duration / 20 * original_sample_rate)
|
||||
if duration / 20 * original_sample_rate > len(original_y)
|
||||
else len(original_y)
|
||||
)
|
||||
]
|
||||
self.res_data[sound_name][pitch] = librosa.resample(
|
||||
deal,
|
||||
orig_sr=original_sample_rate,
|
||||
target_sr=self.output_sample_rate,
|
||||
fix=False,
|
||||
)
|
||||
else:
|
||||
# if self.mode == 1:
|
||||
# 重采样, 不变调
|
||||
# print(">>", sound_name, pitch)
|
||||
self.res_data[sound_name][pitch] = librosa.resample(
|
||||
original_y,
|
||||
orig_sr=original_sample_rate,
|
||||
target_sr=self.output_sample_rate,
|
||||
fix=False, # 尽管这样不变调,但是也不应将 pitch 与打击乐器联系在一起
|
||||
) # 已经在下面重新用 0 取代了打击乐器的 pitch
|
||||
"""elif self.mode == 0:
|
||||
# 重采样, 不变调, 衰弱
|
||||
self.cache_dict[raw_name] = librosa_resample(
|
||||
y_orig,
|
||||
orig_sr=sr_orig,
|
||||
target_sr=self.out_sr,
|
||||
fix=False
|
||||
)"""
|
||||
|
||||
def make_pitch_accurate(self, pitch: float) -> int:
|
||||
"""
|
||||
相信你知道的,小数并不准确
|
||||
"""
|
||||
return int((pitch + self.pitch_deviation) * (10**self.pitch_precision_decimals))
|
||||
|
||||
# 获取资源方法,需要指定采样率,采样率变了即全局重载
|
||||
# 资源存储类里面有就取,没就写入并标识已有数据
|
||||
def get_res(
|
||||
self,
|
||||
sound_name: str,
|
||||
pitch: float,
|
||||
percussive: bool,
|
||||
duration: int,
|
||||
):
|
||||
|
||||
# print(
|
||||
# sound_name,
|
||||
# pitch,
|
||||
# ">>",
|
||||
# (pitch := self.make_pitch_accurate(pitch)),
|
||||
# percussive,
|
||||
# duration,
|
||||
# )
|
||||
pitch = 0 if percussive else self.make_pitch_accurate(pitch)
|
||||
|
||||
if sound_name in self.res_data:
|
||||
if pitch in self.res_data[sound_name]:
|
||||
return self.res_data[sound_name][pitch]
|
||||
else:
|
||||
self.res_shift(
|
||||
sound_name=sound_name,
|
||||
percussive=percussive,
|
||||
pitch=pitch,
|
||||
duration=duration,
|
||||
)
|
||||
# print(2, self.res_data[packageID][soundID][pitch])
|
||||
return self.res_data[sound_name][pitch]
|
||||
else:
|
||||
self.res_data[sound_name] = {}
|
||||
self.res_shift(
|
||||
sound_name=sound_name,
|
||||
percussive=percussive,
|
||||
pitch=pitch,
|
||||
duration=duration,
|
||||
)
|
||||
# print(2, self.res_data[packageID][soundID][pitch])
|
||||
return self.res_data[sound_name][pitch]
|
||||
|
||||
def convert(self, music: SingleMusic, start_tick: float = 0, end_tick: float = inf):
|
||||
|
||||
if self.overlay_mode_c == 1:
|
||||
|
||||
def overlay(seg_overlay: np.ndarray, pos_tick: int):
|
||||
pos_ = int(out_sr * pos_tick * 0.05)
|
||||
wav_model[pos_ : seg_overlay.size + pos_] += seg_overlay
|
||||
|
||||
# print(0)
|
||||
wav_model = np.zeros(
|
||||
int(
|
||||
max(
|
||||
[
|
||||
(
|
||||
i.start_tick * 0.05 * self.output_sample_rate
|
||||
+ len(
|
||||
self.get_res(
|
||||
sound_name=i.instrument,
|
||||
pitch=i.pitch
|
||||
- 60
|
||||
- MM_INSTRUMENT_DEVIATION_TABLE.get(
|
||||
i.instrument, 6
|
||||
),
|
||||
percussive=i.percussive,
|
||||
duration=i.duration_tick,
|
||||
)
|
||||
)
|
||||
)
|
||||
for i in music.get_minenotes(
|
||||
start_time=start_tick, end_time=end_tick
|
||||
)
|
||||
]
|
||||
)
|
||||
),
|
||||
dtype=np.float32,
|
||||
)
|
||||
|
||||
elif self.overlay_mode_c == 2:
|
||||
|
||||
def overlay(seg_overlay: np.ndarray, pos_tick: int):
|
||||
pos_ = int(out_sr * pos_tick * 0.05)
|
||||
wav_model[..., pos_ : len(seg_overlay[0]) + pos_] += seg_overlay
|
||||
|
||||
wav_model = np.zeros(
|
||||
(
|
||||
2,
|
||||
int(
|
||||
max(
|
||||
[
|
||||
(
|
||||
i.start_tick * 0.05 * self.output_sample_rate
|
||||
+ len(
|
||||
self.get_res(
|
||||
sound_name=i.instrument,
|
||||
pitch=i.pitch
|
||||
- 60
|
||||
- MM_INSTRUMENT_DEVIATION_TABLE.get(
|
||||
i.instrument, 6
|
||||
),
|
||||
percussive=i.percussive,
|
||||
duration=i.duration_tick,
|
||||
)
|
||||
)
|
||||
)
|
||||
for i in music.get_minenotes(
|
||||
start_time=start_tick, end_time=end_tick
|
||||
)
|
||||
]
|
||||
)
|
||||
),
|
||||
),
|
||||
dtype=np.float32,
|
||||
)
|
||||
|
||||
else:
|
||||
raise ValueError("错误的 overlay_mode 参数")
|
||||
|
||||
out_sr = self.output_sample_rate
|
||||
|
||||
for note in music.get_minenotes(start_time=start_tick, end_time=end_tick):
|
||||
accurate_pitch = (
|
||||
0
|
||||
if note.percussive
|
||||
else self.make_pitch_accurate(
|
||||
note.pitch
|
||||
- 60
|
||||
- MM_INSTRUMENT_DEVIATION_TABLE.get(note.instrument, 6)
|
||||
)
|
||||
)
|
||||
# print(
|
||||
# ":",
|
||||
# note.instrument,
|
||||
# note.pitch,
|
||||
# ">>",
|
||||
# accurate_pitch,
|
||||
# note.percussive,
|
||||
# note.duration_tick,
|
||||
# )
|
||||
# print(note.position)
|
||||
# print("参数倍率", self.music_volume, (1-(note.position.sound_distance / 48)), flush=True)
|
||||
|
||||
# print("部分数据(前):",self.res_data[note.instrument][accurate_pitch][:5])
|
||||
overlay(
|
||||
self.res_data[note.instrument][accurate_pitch]
|
||||
* self.music_volume
|
||||
* (1 - (note.position.sound_distance / 48)),
|
||||
note.start_tick,
|
||||
)
|
||||
# print("部分数据(后):", wav_model[:5])
|
||||
# 上述参数的原顺序应为
|
||||
# x * (音量 / 127) * (1 / (距离 + 0.5))
|
||||
# 乘法优先是为了提高计算精度,小的数的除法优先同理
|
||||
# 下面是后来的注释
|
||||
# 这种方式是错误的 —— 金羿 20260720
|
||||
|
||||
if self.get_value_method == 0:
|
||||
# 归一化,抚摸耳朵 (bushi
|
||||
max_val = np.max(np.abs(wav_model))
|
||||
if not max_val == 0:
|
||||
wav_model = wav_model / max_val
|
||||
elif self.get_value_method == 1:
|
||||
wav_model[wav_model > 1] = 1
|
||||
wav_model[wav_model < -1] = -1
|
||||
if self.overlay_mode_c == 2:
|
||||
return wav_model.T
|
||||
else:
|
||||
return wav_model[:, np.newaxis]
|
||||
|
||||
def stream(self, music: SingleMusic):
|
||||
pass
|
||||
|
||||
# debug
|
||||
def to_wav_file(self, single_music: SingleMusic, out_file_path: Path):
|
||||
|
||||
sf.write(
|
||||
out_file_path,
|
||||
self.convert(single_music),
|
||||
samplerate=self.output_sample_rate,
|
||||
format="wav",
|
||||
)
|
||||
return out_file_path
|
||||
|
||||
def to_wav_file_byte(self, single_music: SingleMusic, output_file: BinaryIO):
|
||||
sf.write(
|
||||
output_file,
|
||||
self.convert(single_music),
|
||||
samplerate=self.output_sample_rate,
|
||||
format="wav",
|
||||
)
|
||||
@@ -0,0 +1,33 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
音·创 v3 内置的 指令生成插件
|
||||
"""
|
||||
|
||||
"""
|
||||
版权所有 © 2026 金羿、玉衡Alioth
|
||||
Copyright © 2026 Eilles, YuhengAlioth
|
||||
|
||||
开源相关声明请见 仓库根目录下的 License.md
|
||||
Terms & Conditions: License.md in the root directory
|
||||
"""
|
||||
|
||||
# 睿乐组织 开发交流群 861684859
|
||||
# Email TriM-Organization@hotmail.com
|
||||
# 若需转载或借鉴 许可声明请查看仓库目录下的 License.md
|
||||
|
||||
from .main import NoteDataConvert2CommandPlugin, MineCommand
|
||||
|
||||
from .progressbar import ProgressBarStyle, DEFAULT_PROGRESSBAR_STYLE
|
||||
|
||||
__all__ = [
|
||||
# "CommandConvertionConfig",
|
||||
# 插件主类
|
||||
"NoteDataConvert2CommandPlugin",
|
||||
# 进度条样式类
|
||||
"ProgressBarStyle",
|
||||
# Minecraft 指令类
|
||||
"MineCommand",
|
||||
# 默认进度条样式
|
||||
"DEFAULT_PROGRESSBAR_STYLE",
|
||||
]
|
||||
@@ -0,0 +1,637 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
音·创 v3 内置的 指令生成插件
|
||||
"""
|
||||
|
||||
"""
|
||||
版权所有 © 2026 金羿、玉衡Alioth
|
||||
Copyright © 2026 Eilles, YuhengAlioth
|
||||
|
||||
开源相关声明请见 仓库根目录下的 License.md
|
||||
Terms & Conditions: License.md in the root directory
|
||||
"""
|
||||
|
||||
# 睿乐组织 开发交流群 861684859
|
||||
# Email TriM-Organization@hotmail.com
|
||||
# 若需转载或借鉴 许可声明请查看仓库目录下的 License.md
|
||||
|
||||
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import BinaryIO, Optional, Dict, List, Callable, Tuple, Mapping
|
||||
from math import ceil
|
||||
|
||||
from Musicreater import SingleMusic, SingleTrack, SingleNote, SoundAtmos, MineNote
|
||||
from Musicreater.plugins import (
|
||||
library_plugin,
|
||||
PluginConfig,
|
||||
PluginMetaInformation,
|
||||
PluginTypes,
|
||||
LibraryPluginBase,
|
||||
)
|
||||
from Musicreater.exceptions import ZeroSpeedError, IllegalMinimumVolumeError
|
||||
from Musicreater._utils import enumerated_stuffcopy_dictionary
|
||||
|
||||
from .progressbar import ProgressBarStyle, DEFAULT_PROGRESSBAR_STYLE, mctick2timestr
|
||||
from .utils import minenote_to_command_parameters
|
||||
|
||||
# @dataclass
|
||||
# class CommandConvertionConfig(PluginConfig):
|
||||
# execute_command_head: str = "execute as {} at @s positioned ~ ~ ~ run "
|
||||
|
||||
|
||||
@dataclass
|
||||
class MineCommand:
|
||||
"""存储单个指令的类"""
|
||||
|
||||
command: str
|
||||
"""指令文本"""
|
||||
conditional: bool = False
|
||||
"""执行是否有条件"""
|
||||
delay: int = 0
|
||||
"""执行的延迟"""
|
||||
annotation: str = ""
|
||||
"""指令注释"""
|
||||
|
||||
def copy(self):
|
||||
return MineCommand(
|
||||
command=self.command,
|
||||
conditional=self.conditional,
|
||||
delay=self.delay,
|
||||
annotation=self.annotation,
|
||||
)
|
||||
|
||||
@property
|
||||
def mcfunction_command_string(self) -> str:
|
||||
"""
|
||||
我的世界函数字符串(包含注释)
|
||||
"""
|
||||
return self.__str__()
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""
|
||||
转为我的世界函数文件格式(包含注释)
|
||||
"""
|
||||
return "# {cdt}<{delay}> {ant}\n{cmd}".format(
|
||||
cdt="[CDT]" if self.conditional else "",
|
||||
delay=self.delay,
|
||||
ant=self.annotation,
|
||||
cmd=self.command,
|
||||
)
|
||||
|
||||
def __eq__(self, other) -> bool:
|
||||
if isinstance(other, self.__class__):
|
||||
# 不比较注释内容
|
||||
return (
|
||||
(self.command == other.command)
|
||||
and (self.conditional == other.conditional)
|
||||
and (self.delay == other.delay)
|
||||
)
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
@library_plugin("notedata_to_command_plugin")
|
||||
class NoteDataConvert2CommandPlugin(LibraryPluginBase):
|
||||
metainfo = PluginMetaInformation(
|
||||
name="音符数据的基岩版指令支持",
|
||||
author="金羿、玉衡Alioth",
|
||||
description="从音符数据转换为我的世界指令相关格式",
|
||||
version=(0, 0, 1),
|
||||
type=PluginTypes.LIBRARY,
|
||||
license="Same as Musicreater",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def generate_progressbar(
|
||||
max_score: int,
|
||||
scoreboard_name: str,
|
||||
music_name: str = "",
|
||||
progressbar_style: ProgressBarStyle = DEFAULT_PROGRESSBAR_STYLE,
|
||||
execute_command_head: str = "execute as {} at @s positioned ~ ~ ~ run ",
|
||||
) -> List[MineCommand]:
|
||||
"""
|
||||
生成进度条
|
||||
|
||||
Parameters
|
||||
----------
|
||||
max_score: int
|
||||
最大的积分值
|
||||
scoreboard_name: str
|
||||
所使用的计分板名称
|
||||
progressbar_style: ProgressBarStyle
|
||||
此参数详见 ../docs/库的生成与功能文档.md#进度条自定义
|
||||
|
||||
Returns
|
||||
-------
|
||||
list[MineCommand,]
|
||||
"""
|
||||
orignal_style_string = progressbar_style.style_base_string
|
||||
"""用于被替换的进度条原始样式"""
|
||||
|
||||
"""
|
||||
| 标识符 | 指定的可变量 |
|
||||
|---------|----------------|
|
||||
| `%%N` | 乐曲名 |
|
||||
| `%^s` | 计分板最大值 |
|
||||
| `%^t` | 曲目总时长 |
|
||||
| `%%s` | 当前计分板值 |
|
||||
| `%%t` | 当前播放时间 |
|
||||
| `%%%` | 当前进度比率 |
|
||||
| `_` | 用以表示进度条占位|
|
||||
| `%*%` | 指定*的动画内容 |
|
||||
"""
|
||||
per_value_in_each = max_score / orignal_style_string.count("_")
|
||||
"""每个进度条代表的分值"""
|
||||
|
||||
result: List[MineCommand] = []
|
||||
|
||||
orignal_style_string = (
|
||||
orignal_style_string.replace("%%N", music_name)
|
||||
.replace("%^s", str(max_score))
|
||||
.replace("%^t", mctick2timestr(max_score))
|
||||
)
|
||||
|
||||
scoreboard_name_part = scoreboard_name[:2]
|
||||
if "%%%" in orignal_style_string:
|
||||
result.append(
|
||||
MineCommand(
|
||||
'scoreboard objectives add {}PercT dummy "百分比计算"'.format(
|
||||
scoreboard_name_part
|
||||
),
|
||||
annotation="新增临时百分比变量",
|
||||
)
|
||||
)
|
||||
result.append(
|
||||
MineCommand(
|
||||
execute_command_head.format(
|
||||
"@a[scores={" + scoreboard_name + "=1..}]"
|
||||
)
|
||||
+ "scoreboard players set MaxScore {} {}".format(
|
||||
scoreboard_name, max_score
|
||||
),
|
||||
annotation="设定音乐最大延迟分数",
|
||||
)
|
||||
)
|
||||
result.append(
|
||||
MineCommand(
|
||||
execute_command_head.format(
|
||||
"@a[scores={" + scoreboard_name + "=1..}]"
|
||||
)
|
||||
+ "scoreboard players set n100 {} 100".format(scoreboard_name),
|
||||
annotation="设置常量100",
|
||||
)
|
||||
)
|
||||
result.append(
|
||||
MineCommand(
|
||||
execute_command_head.format(
|
||||
"@a[scores={" + scoreboard_name + "=1..}]"
|
||||
)
|
||||
+ "scoreboard players operation @s {} = @s {}".format(
|
||||
scoreboard_name_part + "PercT", scoreboard_name
|
||||
),
|
||||
annotation="赋值当前进度",
|
||||
)
|
||||
)
|
||||
result.append(
|
||||
MineCommand(
|
||||
execute_command_head.format(
|
||||
"@a[scores={" + scoreboard_name + "=1..}]"
|
||||
)
|
||||
+ "scoreboard players operation @s {} *= n100 {}".format(
|
||||
scoreboard_name_part + "PercT", scoreboard_name
|
||||
),
|
||||
annotation="转换当前进度之单位至百分比(扩大精度)",
|
||||
)
|
||||
)
|
||||
result.append(
|
||||
MineCommand(
|
||||
execute_command_head.format(
|
||||
"@a[scores={" + scoreboard_name + "=1..}]"
|
||||
)
|
||||
+ "scoreboard players operation @s {} /= MaxScore {}".format(
|
||||
scoreboard_name_part + "PercT", scoreboard_name
|
||||
),
|
||||
annotation="计算进度百分比",
|
||||
)
|
||||
)
|
||||
|
||||
if "%%t" in orignal_style_string:
|
||||
result.append(
|
||||
MineCommand(
|
||||
'scoreboard objectives add {}TMinT dummy "时间计算:分"'.format(
|
||||
scoreboard_name_part
|
||||
),
|
||||
annotation="新增临时分变量",
|
||||
)
|
||||
)
|
||||
result.append(
|
||||
MineCommand(
|
||||
'scoreboard objectives add {}TSecT dummy "时间计算:秒"'.format(
|
||||
scoreboard_name_part
|
||||
),
|
||||
annotation="新增临时秒变量",
|
||||
)
|
||||
)
|
||||
result.append(
|
||||
MineCommand(
|
||||
execute_command_head.format(
|
||||
"@a[scores={" + scoreboard_name + "=1..}]"
|
||||
)
|
||||
+ "scoreboard players set n20 {} 20".format(scoreboard_name),
|
||||
annotation="设置常量 20",
|
||||
)
|
||||
)
|
||||
result.append(
|
||||
MineCommand(
|
||||
execute_command_head.format(
|
||||
"@a[scores={" + scoreboard_name + "=1..}]"
|
||||
)
|
||||
+ "scoreboard players set n60 {} 60".format(scoreboard_name),
|
||||
annotation="设置常量 60",
|
||||
)
|
||||
)
|
||||
|
||||
result.append(
|
||||
MineCommand(
|
||||
execute_command_head.format(
|
||||
"@a[scores={" + scoreboard_name + "=1..}]"
|
||||
)
|
||||
+ "scoreboard players operation @s {} = @s {}".format(
|
||||
scoreboard_name_part + "TMinT", scoreboard_name
|
||||
),
|
||||
annotation="赋值临时分变量",
|
||||
)
|
||||
)
|
||||
result.append(
|
||||
MineCommand(
|
||||
execute_command_head.format(
|
||||
"@a[scores={" + scoreboard_name + "=1..}]"
|
||||
)
|
||||
+ "scoreboard players operation @s {} /= n20 {}".format(
|
||||
scoreboard_name_part + "TMinT", scoreboard_name
|
||||
),
|
||||
annotation="转换临时分变量之单位为秒(缩减精度)",
|
||||
)
|
||||
)
|
||||
result.append(
|
||||
MineCommand(
|
||||
execute_command_head.format(
|
||||
"@a[scores={" + scoreboard_name + "=1..}]"
|
||||
)
|
||||
+ "scoreboard players operation @s {} = @s {}".format(
|
||||
scoreboard_name_part + "TSecT", scoreboard_name_part + "TMinT"
|
||||
),
|
||||
annotation="赋值临时秒",
|
||||
)
|
||||
)
|
||||
|
||||
result.append(
|
||||
MineCommand(
|
||||
execute_command_head.format(
|
||||
"@a[scores={" + scoreboard_name + "=1..}]"
|
||||
)
|
||||
+ "scoreboard players operation @s {} /= n60 {}".format(
|
||||
scoreboard_name_part + "TMinT", scoreboard_name
|
||||
),
|
||||
annotation="转换临时分变量之单位为分(缩减精度)",
|
||||
)
|
||||
)
|
||||
|
||||
result.append(
|
||||
MineCommand(
|
||||
execute_command_head.format(
|
||||
"@a[scores={" + scoreboard_name + "=1..}]"
|
||||
)
|
||||
+ "scoreboard players operation @s {} %= n60 {}".format(
|
||||
scoreboard_name_part + "TSecT", scoreboard_name
|
||||
),
|
||||
annotation="确定临时秒(框定精度区间)",
|
||||
)
|
||||
)
|
||||
|
||||
if progressbar_style.is_animate_autoloop and progressbar_style.animate_circle:
|
||||
|
||||
result.append(
|
||||
MineCommand(
|
||||
'scoreboard objectives add {}AniC dummy "动画循环控制"'.format(
|
||||
scoreboard_name_part
|
||||
),
|
||||
annotation="新增动画循环控制变量",
|
||||
)
|
||||
)
|
||||
for animate_placeholder in progressbar_style.animate_circle:
|
||||
max_loop_score = max(
|
||||
progressbar_style.animate_circle[animate_placeholder].keys()
|
||||
)
|
||||
if ("%%%" not in orignal_style_string or max_loop_score != 100) and (
|
||||
"%%t" not in orignal_style_string or max_loop_score not in (60, 20)
|
||||
):
|
||||
result.append(
|
||||
MineCommand(
|
||||
execute_command_head.format(
|
||||
"@a[scores={" + scoreboard_name + "=1..}]"
|
||||
)
|
||||
+ "scoreboard players set n{num} {sbn} {num}".format(
|
||||
sbn=scoreboard_name,
|
||||
num=max_loop_score,
|
||||
),
|
||||
annotation="设置常量 {num}".format(num=max_loop_score),
|
||||
)
|
||||
)
|
||||
|
||||
result.append(
|
||||
MineCommand(
|
||||
execute_command_head.format(
|
||||
"@a[scores={" + scoreboard_name + "=1..}]"
|
||||
)
|
||||
+ "scoreboard players operation @s {sbnp}AniC = @s {sbn}".format(
|
||||
sbnp=scoreboard_name_part, sbn=scoreboard_name
|
||||
),
|
||||
)
|
||||
)
|
||||
result.append(
|
||||
MineCommand(
|
||||
execute_command_head.format(
|
||||
"@a[scores={" + scoreboard_name + "=1..}]"
|
||||
)
|
||||
+ "scoreboard players operation @s {sbnp}AniC %= n{num} {sbn}".format(
|
||||
sbnp=scoreboard_name_part,
|
||||
num=max_loop_score,
|
||||
sbn=scoreboard_name,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
for i in range(orignal_style_string.count("_")):
|
||||
npg_stl = (
|
||||
orignal_style_string.replace(
|
||||
"%%s",
|
||||
'"},{"score":{"name":"*","objective":"'
|
||||
+ scoreboard_name
|
||||
+ '"}},{"text":"',
|
||||
)
|
||||
.replace(
|
||||
"%%t",
|
||||
'"},{"score":{"name":"*","objective":"{-}TMinT"}},{"text":":"},'
|
||||
'{"score":{"name":"*","objective":"{-}TSecT"}},{"text":"'.replace(
|
||||
"{-}", scoreboard_name_part
|
||||
),
|
||||
)
|
||||
.replace(
|
||||
"%%%",
|
||||
'"},{"score":{"name":"*","objective":"'
|
||||
+ scoreboard_name_part
|
||||
+ 'PercT"}},{"text":"%',
|
||||
)
|
||||
.replace("_", progressbar_style.progress_played, i + 1)
|
||||
.replace("_", progressbar_style.progress_toplay)
|
||||
)
|
||||
for animate_placeholder in progressbar_style.animate_circle:
|
||||
animation_start_tick = 0
|
||||
npg_stl = npg_stl.replace(
|
||||
animate_placeholder,
|
||||
'"},{"translate": "%%'
|
||||
+ str(
|
||||
len(progressbar_style.animate_circle[animate_placeholder]) + 1
|
||||
)
|
||||
+ '","with":{"rawtext":['
|
||||
+ (
|
||||
",".join(
|
||||
(
|
||||
'{"selector":"@s[scores={{-}={*}..{&}}]"}'.replace(
|
||||
"{*}", str(animation_start_tick)
|
||||
).replace("{&}", str(animation_start_tick := end_tick))
|
||||
for end_tick in progressbar_style.animate_circle[
|
||||
animate_placeholder
|
||||
].keys()
|
||||
)
|
||||
).replace(
|
||||
"{-}",
|
||||
(
|
||||
(scoreboard_name_part + "AniC")
|
||||
if progressbar_style.is_animate_autoloop
|
||||
else scoreboard_name
|
||||
),
|
||||
)
|
||||
)
|
||||
+ ","
|
||||
+ ",".join(
|
||||
(
|
||||
'{"text":"' + animation_text + '"}'
|
||||
for animation_text in progressbar_style.animate_circle[
|
||||
animate_placeholder
|
||||
].values()
|
||||
)
|
||||
)
|
||||
+ ',{"text":"NaN"}]}},{"text":"',
|
||||
)
|
||||
result.append(
|
||||
MineCommand(
|
||||
execute_command_head.format(
|
||||
"@a[scores={"
|
||||
+ scoreboard_name
|
||||
+ f"={int(i * per_value_in_each)}..{ceil((i + 1) * per_value_in_each)}"
|
||||
+ "}]"
|
||||
)
|
||||
+ 'titleraw @s actionbar {"rawtext":[{"text":"'
|
||||
+ npg_stl
|
||||
+ '"}]}',
|
||||
annotation="进度条显示",
|
||||
)
|
||||
)
|
||||
|
||||
if "%%%" in orignal_style_string:
|
||||
result.append(
|
||||
MineCommand(
|
||||
"scoreboard objectives remove {}PercT".format(scoreboard_name_part),
|
||||
annotation="移除临时百分比变量",
|
||||
)
|
||||
)
|
||||
if "%%t" in orignal_style_string:
|
||||
result.append(
|
||||
MineCommand(
|
||||
"scoreboard objectives remove {}TMinT".format(scoreboard_name_part),
|
||||
annotation="移除临时分变量",
|
||||
)
|
||||
)
|
||||
result.append(
|
||||
MineCommand(
|
||||
"scoreboard objectives remove {}TSecT".format(scoreboard_name_part),
|
||||
annotation="移除临时秒变量",
|
||||
)
|
||||
)
|
||||
if progressbar_style.is_animate_autoloop and progressbar_style.animate_circle:
|
||||
result.append(
|
||||
MineCommand(
|
||||
"scoreboard objectives remove {}AniC".format(scoreboard_name_part),
|
||||
annotation="移除临时动画循环控制变量",
|
||||
)
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def to_command_list_in_score(
|
||||
music: SingleMusic,
|
||||
music_deviation: float = 0,
|
||||
minimum_volume: float = 0.01,
|
||||
scoreboard_name: str = "mscplay",
|
||||
execute_command_head: str = "execute as {} at @s positioned ~ ~ ~ run ",
|
||||
) -> Tuple[List[List[MineCommand]], int, int]:
|
||||
"""
|
||||
将midi转换为我的世界命令列表
|
||||
|
||||
Parameters
|
||||
----------
|
||||
scoreboard_name: str
|
||||
我的世界的计分板名称
|
||||
|
||||
Returns
|
||||
-------
|
||||
tuple( list[list[MineCommand指令,... ],... ], int指令数量, int音乐时长游戏刻 )
|
||||
"""
|
||||
|
||||
command_channels: List[List[MineCommand]] = []
|
||||
command_amount = 0
|
||||
max_score = 0
|
||||
|
||||
for track in music.music_tracks:
|
||||
# 如果当前轨道为空 则跳过
|
||||
if not track:
|
||||
continue
|
||||
|
||||
this_channel = []
|
||||
|
||||
for note in track.minenotes:
|
||||
max_score = max(max_score, note.start_tick)
|
||||
|
||||
(
|
||||
relative_coordinates,
|
||||
mc_pitch,
|
||||
) = minenote_to_command_parameters(
|
||||
note,
|
||||
pitch_deviation=music_deviation,
|
||||
)
|
||||
|
||||
this_channel.append(
|
||||
MineCommand(
|
||||
(
|
||||
execute_command_head.format(
|
||||
"@a[scores=({}={})]".format(
|
||||
scoreboard_name, note.start_tick
|
||||
)
|
||||
.replace("(", r"{")
|
||||
.replace(")", r"}")
|
||||
)
|
||||
+ "playsound {} @s ^{} ^{} ^{} 3.0 {} {}".format(
|
||||
track.instrument,
|
||||
*relative_coordinates,
|
||||
1.0 if note.percussive else mc_pitch,
|
||||
minimum_volume,
|
||||
)
|
||||
),
|
||||
annotation=(
|
||||
"[{}] 打击乐音符{}".format(
|
||||
mctick2timestr(note.start_tick),
|
||||
note.instrument,
|
||||
)
|
||||
if note.percussive
|
||||
else "[{}] 音符{}:{:.2f}".format(
|
||||
mctick2timestr(note.start_tick),
|
||||
note.instrument,
|
||||
mc_pitch,
|
||||
)
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
command_amount += 1
|
||||
|
||||
if this_channel:
|
||||
command_channels.append(this_channel)
|
||||
|
||||
return command_channels, command_amount, max_score
|
||||
|
||||
@staticmethod
|
||||
def to_command_list_in_delay(
|
||||
music: SingleMusic,
|
||||
music_deviation: float = 0,
|
||||
minimum_volume: float = 0.01,
|
||||
player_selector: str = "@a",
|
||||
execute_command_head: str = "execute as {} at @s positioned ~ ~ ~ run ",
|
||||
) -> Tuple[List[MineCommand], int, int]:
|
||||
"""
|
||||
将midi转换为我的世界命令列表,并输出每个音符之后的延迟
|
||||
|
||||
Parameters
|
||||
----------
|
||||
player_selector: str
|
||||
玩家选择器,默认为`@a`
|
||||
|
||||
Returns
|
||||
-------
|
||||
tuple( list[MineCommand指令,...], int音乐时长游戏刻, int最大同时播放的指令数量 )
|
||||
"""
|
||||
|
||||
# 音轨判断
|
||||
music_command_list = []
|
||||
multi = max_multi = 0
|
||||
delaytime_previous = 0
|
||||
last_note: MineNote
|
||||
|
||||
for note in music.get_minenotes(
|
||||
start_time=0,
|
||||
):
|
||||
if (tickdelay := (note.precise_tick - delaytime_previous)) == 0:
|
||||
multi += 1
|
||||
else:
|
||||
max_multi = max(max_multi, multi)
|
||||
multi = 0
|
||||
|
||||
(
|
||||
relative_coordinates,
|
||||
mc_pitch,
|
||||
) = minenote_to_command_parameters(
|
||||
note,
|
||||
pitch_deviation=music_deviation,
|
||||
)
|
||||
|
||||
music_command_list.append(
|
||||
MineCommand(
|
||||
command=(
|
||||
execute_command_head.format(player_selector)
|
||||
+ "playsound {} @s ^{} ^{} ^{} 3.0 {} {}".format(
|
||||
note.instrument,
|
||||
*relative_coordinates,
|
||||
1.0 if note.percussive else mc_pitch,
|
||||
minimum_volume,
|
||||
)
|
||||
),
|
||||
annotation=(
|
||||
"[{}] 打击乐音符{}".format(
|
||||
mctick2timestr(note.start_tick),
|
||||
note.instrument,
|
||||
)
|
||||
if note.percussive
|
||||
else "[{}] 音符{}:{:.2f}".format(
|
||||
mctick2timestr(note.start_tick),
|
||||
note.instrument,
|
||||
mc_pitch,
|
||||
)
|
||||
),
|
||||
delay=int(tickdelay),
|
||||
),
|
||||
)
|
||||
delaytime_previous = note.precise_tick
|
||||
last_note = note
|
||||
if music_command_list:
|
||||
return (
|
||||
music_command_list,
|
||||
last_note.start_tick + last_note.duration_tick,
|
||||
max_multi + 1,
|
||||
)
|
||||
else:
|
||||
return [], 0, 0
|
||||
@@ -0,0 +1,179 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
音·创 v3 内置的 指令生成插件的进度条相关内容
|
||||
"""
|
||||
|
||||
"""
|
||||
版权所有 © 2026 金羿、玉衡Alioth
|
||||
Copyright © 2026 Eilles, YuhengAlioth
|
||||
|
||||
开源相关声明请见 仓库根目录下的 License.md
|
||||
Terms & Conditions: License.md in the root directory
|
||||
"""
|
||||
|
||||
# 睿乐组织 开发交流群 861684859
|
||||
# Email TriM-Organization@hotmail.com
|
||||
# 若需转载或借鉴 许可声明请查看仓库目录下的 License.md
|
||||
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import BinaryIO, Optional, Dict, List, Callable, Tuple, Mapping
|
||||
|
||||
|
||||
# 这个类也有很大的优化空间a
|
||||
@dataclass(init=False)
|
||||
class ProgressBarStyle:
|
||||
"""进度条样式类"""
|
||||
|
||||
style_base_string: str
|
||||
"""基础样式"""
|
||||
|
||||
progress_toplay: str
|
||||
"""未播放之样式"""
|
||||
|
||||
progress_played: str
|
||||
"""已播放之样式"""
|
||||
|
||||
is_animate_autoloop: bool
|
||||
"""所示动画是否循环"""
|
||||
|
||||
animate_circle: Dict[str, Dict[int, str]]
|
||||
"""
|
||||
定义动画样式
|
||||
Dict[占位符, Dict[截止时间刻, 样式字符串]]
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
base_string: str = "【%%N】%A%▶ %%s/%^s (%%t|%^t) \n"
|
||||
"[§e_________________________§r] %%%",
|
||||
to_play_style: str = "§7=",
|
||||
played_style: str = "=",
|
||||
animate_loop: bool = True,
|
||||
animate_circle: Dict[str, Dict[int, str]] = {
|
||||
"%A%": {5: "-", 10: "\\\\", 15: "|", 20: "/"}
|
||||
},
|
||||
):
|
||||
"""
|
||||
用于存储进度条样式的类,标识符替换顺序如下表
|
||||
|
||||
| 标识符 | 指定的可变量 |
|
||||
|---------|----------------|
|
||||
| `%%N` | 乐曲名 |
|
||||
| `%^s` | 计分板最大值 |
|
||||
| `%^t` | 曲目总时长 |
|
||||
| `%%s` | 当前计分板值 |
|
||||
| `%%t` | 当前播放时间 |
|
||||
| `%%%` | 当前进度比率 |
|
||||
| `_` | 用以表示进度条占位|
|
||||
| `%*%` | 指定*的动画内容 |
|
||||
|
||||
Parameters
|
||||
------------
|
||||
base_string: str
|
||||
基础样式,用以定义进度条整体
|
||||
to_play_style: str
|
||||
进度条样式:尚未播放的样子
|
||||
played_style: str
|
||||
已经播放的样子
|
||||
|
||||
Returns
|
||||
---------
|
||||
ProgressBarStyle 类
|
||||
"""
|
||||
|
||||
self.style_base_string = base_string
|
||||
self.progress_toplay = to_play_style
|
||||
self.progress_played = played_style
|
||||
self.is_animate_autoloop = animate_loop
|
||||
self.animate_circle = animate_circle
|
||||
|
||||
def set_base_style(self, value: str):
|
||||
"""设置基础样式"""
|
||||
self.style_base_string = value
|
||||
|
||||
def set_to_play_style(self, value: str):
|
||||
"""设置未播放之样式"""
|
||||
self.progress_toplay = value
|
||||
|
||||
def set_played_style(self, value: str):
|
||||
"""设置已播放之样式"""
|
||||
self.progress_played = value
|
||||
|
||||
def copy(self):
|
||||
return ProgressBarStyle(
|
||||
self.style_base_string,
|
||||
self.progress_toplay,
|
||||
self.progress_played,
|
||||
self.is_animate_autoloop,
|
||||
self.animate_circle,
|
||||
)
|
||||
|
||||
def play_output(
|
||||
self,
|
||||
played_ticks: int,
|
||||
total_ticks: int,
|
||||
music_name: str = "无题",
|
||||
) -> str:
|
||||
"""
|
||||
直接依照此格式输出一个进度条
|
||||
|
||||
Parameters
|
||||
------------
|
||||
played_delays: int
|
||||
当前播放进度积分值
|
||||
total_delays: int
|
||||
乐器总延迟数(计分板值)
|
||||
music_name: str
|
||||
曲名
|
||||
|
||||
Returns
|
||||
---------
|
||||
str
|
||||
进度条字符串
|
||||
"""
|
||||
|
||||
alpha_string = (
|
||||
self.style_base_string.replace("%%N", music_name)
|
||||
.replace("%%s", str(played_ticks))
|
||||
.replace("%^s", str(total_ticks))
|
||||
.replace("%%t", mctick2timestr(played_ticks))
|
||||
.replace("%^t", mctick2timestr(total_ticks))
|
||||
.replace(
|
||||
"%%%",
|
||||
"{:0>5.2f}%".format(int(10000 * played_ticks / total_ticks) / 100),
|
||||
)
|
||||
.replace(
|
||||
"_",
|
||||
self.progress_played,
|
||||
(played_ticks * self.style_base_string.count("_") // total_ticks) + 1,
|
||||
)
|
||||
.replace("_", self.progress_toplay)
|
||||
)
|
||||
|
||||
for key, animate_dict in self.animate_circle.items():
|
||||
max_animate_tick = max(animate_dict.keys())
|
||||
if self.is_animate_autoloop:
|
||||
animate_time_key = 0
|
||||
for time_key in animate_dict.keys():
|
||||
animate_time_key = time_key
|
||||
if time_key > played_ticks % max_animate_tick:
|
||||
break
|
||||
else:
|
||||
animate_time_key = max_animate_tick
|
||||
alpha_string = alpha_string.replace(key, animate_dict[animate_time_key])
|
||||
return alpha_string
|
||||
|
||||
|
||||
def mctick2timestr(mc_tick: int) -> str:
|
||||
"""
|
||||
将《我的世界》的游戏刻计转为表示时间的字符串
|
||||
"""
|
||||
return "{:0>2d}:{:0>2d}".format(mc_tick // 1200, (mc_tick // 20) % 60)
|
||||
|
||||
|
||||
DEFAULT_PROGRESSBAR_STYLE = ProgressBarStyle()
|
||||
"""
|
||||
默认的进度条样式
|
||||
"""
|
||||
@@ -0,0 +1,113 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
音·创 v3 内置的指令生成插件的功能方法
|
||||
"""
|
||||
|
||||
"""
|
||||
版权所有 © 2026 金羿、玉衡Alioth
|
||||
Copyright © 2026 Eilles, YuhengAlioth
|
||||
|
||||
开源相关声明请见 仓库根目录下的 License.md
|
||||
Terms & Conditions: License.md in the root directory
|
||||
"""
|
||||
|
||||
# 睿乐组织 开发交流群 861684859
|
||||
# Email TriM-Organization@hotmail.com
|
||||
# 若需转载或借鉴 许可声明请查看仓库目录下的 License.md
|
||||
|
||||
from typing import (
|
||||
BinaryIO,
|
||||
Optional,
|
||||
Dict,
|
||||
List,
|
||||
Callable,
|
||||
Tuple,
|
||||
Mapping,
|
||||
Union,
|
||||
Literal,
|
||||
)
|
||||
|
||||
from Musicreater import MineNote, SingleNote
|
||||
from Musicreater.constants import MM_INSTRUMENT_DEVIATION_TABLE
|
||||
|
||||
|
||||
# 这个函数可以直接被优化成一个只处理音调参数的,没必要完整留着
|
||||
def minenote_to_command_parameters(
|
||||
mine_note: MineNote,
|
||||
pitch_deviation: float = 0,
|
||||
) -> Tuple[
|
||||
Tuple[float, float, float],
|
||||
Union[float, Literal[None]],
|
||||
]:
|
||||
"""
|
||||
将 MineNote 对象转为《我的世界》音符播放所需之参数
|
||||
|
||||
参数
|
||||
----
|
||||
mine_note: MineNote
|
||||
我的世界音符对象
|
||||
pitch_deviation: float
|
||||
音调偏移量
|
||||
|
||||
返回
|
||||
----
|
||||
tuple[float, float, float], float
|
||||
播放视角坐标, 指令音调参数
|
||||
"""
|
||||
|
||||
return (
|
||||
mine_note.position.position_displacement,
|
||||
(
|
||||
None
|
||||
if mine_note.percussive
|
||||
else (
|
||||
2
|
||||
** (
|
||||
(
|
||||
mine_note.pitch
|
||||
- 60
|
||||
- MM_INSTRUMENT_DEVIATION_TABLE.get(mine_note.instrument, 6)
|
||||
+ pitch_deviation
|
||||
)
|
||||
/ 12
|
||||
)
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def calculate_minecraft_pitch(
|
||||
note: MineNote, pitch_deviation: float = 0
|
||||
) -> Optional[float]:
|
||||
"""
|
||||
计算音符的音调参数
|
||||
|
||||
参数
|
||||
----
|
||||
note: MineNote
|
||||
我的世界音符对象
|
||||
deviation: float
|
||||
音调偏移量
|
||||
|
||||
返回
|
||||
----
|
||||
Optional[float]
|
||||
音调参数, 当为打击乐器时为 None
|
||||
"""
|
||||
return (
|
||||
None
|
||||
if note.percussive
|
||||
else (
|
||||
2
|
||||
** (
|
||||
(
|
||||
note.pitch
|
||||
- 60
|
||||
- MM_INSTRUMENT_DEVIATION_TABLE.get(note.instrument, 6)
|
||||
+ pitch_deviation
|
||||
)
|
||||
/ 12
|
||||
)
|
||||
)
|
||||
)
|
||||
+135
-957
File diff suppressed because it is too large
Load Diff
+723
-155
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
存储 音·创 v3 用到的一些报错类型
|
||||
音·创 v3 用到的一些报错类型
|
||||
"""
|
||||
|
||||
"""
|
||||
@@ -73,6 +73,15 @@ class InnerlyParameterError(MusicreaterInnerlyError):
|
||||
super().__init__("传参错误 - ", *args)
|
||||
|
||||
|
||||
class ParameterCacheUnfreshError(InnerlyParameterError):
|
||||
"""参数缓存未刷新"""
|
||||
|
||||
def __init__(self, *args):
|
||||
"""参数缓存未刷新"""
|
||||
super().__init__("参数缓存未刷新:", *args)
|
||||
|
||||
|
||||
|
||||
class ParameterTypeError(InnerlyParameterError, TypeError):
|
||||
"""参数类型错误"""
|
||||
|
||||
@@ -106,10 +115,10 @@ class OuterlyParameterError(MusicreaterOuterlyError):
|
||||
|
||||
|
||||
class ZeroSpeedError(OuterlyParameterError, ZeroDivisionError):
|
||||
"""以0作为播放速度的错误"""
|
||||
"""以 0 作为播放速度的错误"""
|
||||
|
||||
def __init__(self, *args):
|
||||
"""以0作为播放速度的错误"""
|
||||
"""以 0 作为播放速度的错误"""
|
||||
super().__init__("播放速度为零:", *args)
|
||||
|
||||
|
||||
@@ -225,12 +234,19 @@ class PluginLoadError(MusicreaterOuterlyError):
|
||||
super().__init__("插件加载错误 - ", *args)
|
||||
|
||||
|
||||
class PluginDependencyNotFound(PluginLoadError):
|
||||
"""插件依赖未找到"""
|
||||
|
||||
def __init__(self, *args):
|
||||
super().__init__("未找到所需的插件依赖:", *args)
|
||||
|
||||
|
||||
class PluginNotFoundError(PluginLoadError):
|
||||
"""插件未找到"""
|
||||
|
||||
def __init__(self, *args):
|
||||
"""插件未找到"""
|
||||
super().__init__("插件未找到:", *args)
|
||||
super().__init__("无法找到插件:", *args)
|
||||
|
||||
|
||||
class PluginRegisteredError(PluginLoadError):
|
||||
@@ -241,7 +257,6 @@ class PluginRegisteredError(PluginLoadError):
|
||||
super().__init__("插件重复注册:", *args)
|
||||
|
||||
|
||||
|
||||
class PluginConfigRelatedError(MusicreaterOuterlyError):
|
||||
"""插件配置相关错误"""
|
||||
|
||||
|
||||
+20
-13
@@ -3,10 +3,11 @@
|
||||
|
||||
"""
|
||||
音·创
|
||||
是一款免费开源的《我的世界》数字音频支持库。
|
||||
是一款免费、开源、高性能、插件化的《我的世界》数字音频支持库。
|
||||
|
||||
Musicreater (音·创)
|
||||
A free and open-source library for handling with **Minecraft** digital music.
|
||||
A cost free, open-source, blazing fast and plugin-
|
||||
supported library for editing _Minecraft_ digital music.
|
||||
|
||||
版权所有 © 2026 睿乐组织
|
||||
Copyright © 2026 TriM-Organization
|
||||
@@ -41,7 +42,7 @@ https://gitee.com/TriM-Organization/Musicreater/blob/master/LICENSE.md。
|
||||
# Bug retreat! Bug retreat!
|
||||
# Exceptions and errors are causing chaos
|
||||
# Words combine! Codes unite!
|
||||
# Hurry to call the programmer! Let's Go!
|
||||
# Hurry to call the Programmer! Let's Go!
|
||||
|
||||
import re
|
||||
|
||||
@@ -63,7 +64,7 @@ from .plugins import (
|
||||
PluginRegistry,
|
||||
PluginConfig,
|
||||
PluginTypes,
|
||||
load_plugin_module,
|
||||
load_plugin_from_module,
|
||||
T_IOPlugin,
|
||||
T_Plugin,
|
||||
)
|
||||
@@ -96,9 +97,10 @@ class MusiCreater:
|
||||
def _get_plugin_within_iousage(
|
||||
get_func: Callable[[Union[Path, str]], Generator[T_IOPlugin, None, None]],
|
||||
fpath: Path,
|
||||
plg_regdict: Dict[str, T_IOPlugin],
|
||||
plg_regdict: Mapping[str, T_IOPlugin],
|
||||
plg_id: Optional[str],
|
||||
) -> T_IOPlugin:
|
||||
"""这个函数是用于从指定的注册表项里面调取实例的,仅供下面这几个函数使用"""
|
||||
|
||||
__plugin: Optional[T_IOPlugin] = None
|
||||
if plg_id:
|
||||
@@ -116,9 +118,16 @@ class MusiCreater:
|
||||
if __plugin:
|
||||
return __plugin
|
||||
else:
|
||||
raise FileFormatNotSupportedError(
|
||||
"无法找到处理`{}`类型文件的插件".format(fpath.suffix.upper())
|
||||
)
|
||||
if plg_id:
|
||||
raise PluginNotFoundError(
|
||||
"无法找到惟一识别码为`{}`、处理`{}`格式的插件".format(
|
||||
plg_id, fpath.suffix.upper()
|
||||
)
|
||||
)
|
||||
else:
|
||||
raise FileFormatNotSupportedError(
|
||||
"无法找到处理`{}`格式的插件".format(fpath.suffix.upper())
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def import_music(
|
||||
@@ -158,7 +167,7 @@ class MusiCreater:
|
||||
plugin_id: Optional[str] = None,
|
||||
plugin_config: Optional[PluginConfig] = None,
|
||||
) -> None:
|
||||
self._get_plugin_within_iousage(
|
||||
return self._get_plugin_within_iousage(
|
||||
self.__plugin_registry.get_music_output_plugin_by_format,
|
||||
file_path,
|
||||
self.__plugin_registry._music_output_plugins,
|
||||
@@ -172,7 +181,7 @@ class MusiCreater:
|
||||
plugin_id: Optional[str] = None,
|
||||
plugin_config: Optional[PluginConfig] = None,
|
||||
) -> None:
|
||||
self._get_plugin_within_iousage(
|
||||
return self._get_plugin_within_iousage(
|
||||
self.__plugin_registry.get_track_output_plugin_by_format,
|
||||
file_path,
|
||||
self.__plugin_registry._track_output_plugins,
|
||||
@@ -257,13 +266,11 @@ class MusiCreater:
|
||||
self._plugin_cache[plg_id] = self._plugin_cache[plugin_name]
|
||||
return self._plugin_cache[plg_id]
|
||||
|
||||
closest = self._get_closest_plugin_id(plg_id)
|
||||
|
||||
raise AttributeError(
|
||||
"插件`{}`不存在,请检查插件的惟一识别码是否正确".format(plg_id)
|
||||
+ (
|
||||
";或者阁下可能想要使用的是`{}`插件?".format(closest)
|
||||
if closest
|
||||
if (closest := self._get_closest_plugin_id(plg_id))
|
||||
else ""
|
||||
)
|
||||
)
|
||||
|
||||
+360
-120
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
存储 音·创 v3 内部数据使用的参数曲线
|
||||
音·创 v3 内部数据使用的参数曲线
|
||||
"""
|
||||
|
||||
"""
|
||||
@@ -24,14 +24,12 @@ Terms & Conditions: License.md in the root directory
|
||||
# 可以等伶伦工作站开发出来后再进行完整的测试
|
||||
|
||||
|
||||
from math import ceil
|
||||
from math import ceil, inf
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional, Any, List, Tuple
|
||||
from typing import Optional, Any, List, Tuple, Callable
|
||||
from enum import Enum
|
||||
import bisect
|
||||
|
||||
from .types import FittingFunctionType
|
||||
|
||||
|
||||
def _evaluate_bezier_segment(
|
||||
t0: float,
|
||||
@@ -178,7 +176,7 @@ class Keyframe:
|
||||
value: float
|
||||
|
||||
# 函数插值模式
|
||||
out_interp: Optional[FittingFunctionType] = None
|
||||
out_interp: Optional[Callable[[float], float]] = None
|
||||
|
||||
# 贝塞尔模式
|
||||
in_tangent: Optional[Tuple[float, float]] = (
|
||||
@@ -189,6 +187,16 @@ class Keyframe:
|
||||
)
|
||||
use_bezier: bool = False
|
||||
|
||||
def copy(self) -> "Keyframe":
|
||||
return Keyframe(
|
||||
time=self.time,
|
||||
value=self.value,
|
||||
out_interp=self.out_interp,
|
||||
in_tangent=self.in_tangent,
|
||||
out_tangent=self.out_tangent,
|
||||
use_bezier=self.use_bezier,
|
||||
)
|
||||
|
||||
|
||||
class BoundaryBehaviour(str, Enum):
|
||||
"""
|
||||
@@ -210,25 +218,36 @@ class ParamCurve:
|
||||
支持动态节点编辑
|
||||
用户通过添加/修改关键帧(时间-值对)来定义曲线,类自动在相邻关键帧之间生成插值段。
|
||||
支持多种插值模式:线性('linear')、平滑缓动('smooth')、保持('hold')或自定义函数。
|
||||
支持值域限制:可设定参数值的上限与下限,所有输出及关键帧写入均受约束。
|
||||
"""
|
||||
|
||||
base_line: float = 0.0
|
||||
"""基线/默认值"""
|
||||
|
||||
base_interpolation_function: FittingFunctionType
|
||||
base_interpolation_function: Callable[[float], float]
|
||||
"""默认(未指定区间时的)关键帧插值模式"""
|
||||
|
||||
boundary_behaviour: BoundaryBehaviour
|
||||
"""边界行为,控制参数曲线在已定义的范围外的返回值"""
|
||||
|
||||
value_min: Optional[float] = None
|
||||
"""参数值下限(None 表示不限制)"""
|
||||
|
||||
value_max: Optional[float] = None
|
||||
"""参数值上限(None 表示不限制)"""
|
||||
|
||||
_keys: List[Keyframe]
|
||||
"""关键帧列表"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
base_value: float = 0.0,
|
||||
default_interpolation_function: FittingFunctionType = InterpolationMethod.linear,
|
||||
default_interpolation_function: Callable[
|
||||
[float], float
|
||||
] = InterpolationMethod.linear,
|
||||
boundary_mode: BoundaryBehaviour = BoundaryBehaviour.CONSTANT,
|
||||
value_min: Optional[float] = None,
|
||||
value_max: Optional[float] = None,
|
||||
):
|
||||
"""
|
||||
初始化参数曲线。
|
||||
@@ -237,27 +256,259 @@ class ParamCurve:
|
||||
----------
|
||||
base_value : float
|
||||
边界外默认值(当 boundary_mode 为 BoundaryBehaviour.CONSTANT 时使用)。
|
||||
default_interpolation_function : FittingFunctionType
|
||||
default_interpolation_function : Callable
|
||||
新关键帧的默认 out_interp。
|
||||
boundary_mode : BoundaryBehaviour
|
||||
范围外行为:
|
||||
- BoundaryBehaviour.CONSTANT: 返回 base_value
|
||||
- BoundaryBehaviour.HOLD: 保持首/尾关键帧值
|
||||
value_min : Optional[float]
|
||||
参数值下限。None 表示无下限。
|
||||
value_max : Optional[float]
|
||||
参数值上限。None 表示无上限。
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
若 value_min 和 value_max 均非 None 且 value_min > value_max。
|
||||
"""
|
||||
self.base_line = base_value
|
||||
# 先设定值域,再设定 base_line(以便 base_line 受约束)
|
||||
self._set_value_range_internal(value_min, value_max)
|
||||
|
||||
self.base_line = self._clamp_value(base_value)
|
||||
self.base_interpolation_function = default_interpolation_function
|
||||
self.boundary_behaviour = boundary_mode
|
||||
|
||||
self._keys: List[Keyframe] = []
|
||||
|
||||
# ──────────────────────────────────────────────
|
||||
# 值域限制 相关
|
||||
# ──────────────────────────────────────────────
|
||||
|
||||
def _set_value_range_internal(
|
||||
self,
|
||||
value_min: Optional[float],
|
||||
value_max: Optional[float],
|
||||
):
|
||||
"""
|
||||
内部方法:设定值域并校验合法性。
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
若 value_min > value_max。
|
||||
"""
|
||||
if value_min is not None and value_max is not None and value_min > value_max:
|
||||
raise ValueError(f"值域下限 ({value_min}) 不得大于上限 ({value_max})。")
|
||||
self.value_min = value_min
|
||||
self.value_max = value_max
|
||||
|
||||
def _clamp_value(self, value: float) -> float:
|
||||
"""
|
||||
将给定值限制在 [value_min, value_max] 范围内。
|
||||
仅在低频操作(add_key、update_key_value、set_value_range 等)中使用。
|
||||
高频路径 value_at() 使用内联版本。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
value : float
|
||||
待限制的值。
|
||||
|
||||
Returns
|
||||
-------
|
||||
float
|
||||
限制后的值。若对应边界为 None,则该方向不做限制。
|
||||
"""
|
||||
if self.value_min is not None and value < self.value_min:
|
||||
return self.value_min
|
||||
if self.value_max is not None and value > self.value_max:
|
||||
return self.value_max
|
||||
return value
|
||||
|
||||
def set_value_range(
|
||||
self,
|
||||
value_min: Optional[float] = None,
|
||||
value_max: Optional[float] = None,
|
||||
clamp_existing_keys: bool = True,
|
||||
):
|
||||
"""
|
||||
设置参数曲线的值域(最大/最小值限制)。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
value_min : Optional[float]
|
||||
参数值下限。传入 None 表示取消下限限制。
|
||||
value_max : Optional[float]
|
||||
参数值上限。传入 None 表示取消上限限制。
|
||||
clamp_existing_keys : bool
|
||||
若为 True(默认),则立即将已有关键帧的值及 base_line
|
||||
限制到新的值域范围内。若为 False,则仅影响后续输出与新增关键帧。
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
若 value_min > value_max。
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> curve = ParamCurve(base_value=0.5)
|
||||
>>> curve.add_key(0.0, 1.5)
|
||||
>>> curve.add_key(1.0, -0.3)
|
||||
>>> curve.set_value_range(0.0, 1.0) # 限制到 [0, 1]
|
||||
>>> curve.value_at(0.0)
|
||||
1.0
|
||||
>>> curve.value_at(1.0)
|
||||
0.0
|
||||
"""
|
||||
self._set_value_range_internal(value_min, value_max)
|
||||
|
||||
if clamp_existing_keys:
|
||||
# 限制 base_line
|
||||
self.base_line = self._clamp_value(self.base_line)
|
||||
# 限制所有已有关键帧的值
|
||||
for i, key in enumerate(self._keys):
|
||||
clamped = self._clamp_value(key.value)
|
||||
if clamped != key.value:
|
||||
self._keys[i] = Keyframe(
|
||||
time=key.time,
|
||||
value=clamped,
|
||||
out_interp=key.out_interp,
|
||||
in_tangent=key.in_tangent,
|
||||
out_tangent=key.out_tangent,
|
||||
use_bezier=key.use_bezier,
|
||||
)
|
||||
|
||||
def get_value_range(self) -> Tuple[Optional[float], Optional[float]]:
|
||||
"""
|
||||
获取当前值域设定。
|
||||
|
||||
Returns
|
||||
-------
|
||||
Tuple[Optional[float], Optional[float]]
|
||||
(value_min, value_max),None 表示该方向无限制。
|
||||
"""
|
||||
return (self.value_min, self.value_max)
|
||||
|
||||
def clear_value_range(self, clamp_existing_keys: bool = False):
|
||||
"""
|
||||
清除值域限制(恢复为无限制状态)。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
clamp_existing_keys : bool
|
||||
通常为 False(清除限制无需 clamp)。保留此参数仅为接口对称。
|
||||
"""
|
||||
self.value_min = None
|
||||
self.value_max = None
|
||||
|
||||
# ──────────────────────────────────────────────
|
||||
# 原有功能
|
||||
# ──────────────────────────────────────────────
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
return bool(self._keys) or (self.base_line != 0)
|
||||
|
||||
def find_key(self, time: float) -> Tuple[int, Optional[Keyframe]]:
|
||||
idx = bisect.bisect_left(self._keys, time, key=lambda k: k.time)
|
||||
if idx < len(self._keys) and self._keys[idx].time == time:
|
||||
return idx, self._keys[idx]
|
||||
else:
|
||||
# print("[警告] ParamCurve.find_key: 找不到指定时间点所对应之关键帧")
|
||||
return idx, None
|
||||
|
||||
def copy(
|
||||
self,
|
||||
start: float = 0,
|
||||
end: float = inf,
|
||||
keep_zone_boundary_value: bool = False,
|
||||
global_boundary_safe: bool = True,
|
||||
) -> "ParamCurve":
|
||||
"""
|
||||
返回参数曲线在某时间段内的副本
|
||||
|
||||
Parameters
|
||||
----------
|
||||
start : float
|
||||
起始时间。
|
||||
end : float
|
||||
结束时间。
|
||||
keep_zone_boundary_value : bool
|
||||
是否保留边界值(即在副本中的当前选区之边界创建以当前曲线值作为值的关键帧)。
|
||||
global_boundary_safe : bool
|
||||
当保留边界值启用时,是否忽略本身就处于参数曲线边界外的值,使其不创建关键帧。
|
||||
|
||||
Returns
|
||||
-------
|
||||
ParamCurve
|
||||
参数曲线之副本
|
||||
"""
|
||||
param_curve = ParamCurve(
|
||||
self.base_line,
|
||||
self.base_interpolation_function,
|
||||
self.boundary_behaviour,
|
||||
value_min=self.value_min,
|
||||
value_max=self.value_max,
|
||||
)
|
||||
if start >= end:
|
||||
return param_curve
|
||||
start_index, starter_keyframe = self.find_key(start)
|
||||
end_index, ender_keyframe = self.find_key(end)
|
||||
if starter_keyframe and ender_keyframe:
|
||||
param_curve._keys = [
|
||||
key.copy() for key in self._keys[start_index : end_index + 1]
|
||||
]
|
||||
elif starter_keyframe:
|
||||
if end_index >= len(self._keys) and global_boundary_safe:
|
||||
param_curve._keys = [key.copy() for key in self._keys[start_index:]]
|
||||
else:
|
||||
param_curve._keys = [
|
||||
key.copy() for key in self._keys[start_index:end_index]
|
||||
]
|
||||
if keep_zone_boundary_value:
|
||||
param_curve.add_key(end, self.value_at(end))
|
||||
elif ender_keyframe:
|
||||
if start_index <= 0 and global_boundary_safe:
|
||||
param_curve._keys = [key.copy() for key in self._keys[:end_index]]
|
||||
else:
|
||||
param_curve._keys = [
|
||||
key.copy() for key in self._keys[start_index : end_index + 1]
|
||||
]
|
||||
if keep_zone_boundary_value:
|
||||
param_curve.add_key(start, self.value_at(start))
|
||||
else:
|
||||
if (
|
||||
start_index <= 0
|
||||
and end_index >= len(self._keys)
|
||||
and global_boundary_safe
|
||||
):
|
||||
param_curve._keys = [key.copy() for key in self._keys]
|
||||
else:
|
||||
param_curve._keys = [
|
||||
key.copy() for key in self._keys[start_index:end_index]
|
||||
]
|
||||
if keep_zone_boundary_value:
|
||||
param_curve.add_key(start, self.value_at(start))
|
||||
param_curve.add_key(end, self.value_at(end))
|
||||
return param_curve
|
||||
|
||||
def delete(self, start: float, end: float):
|
||||
"""
|
||||
删除参数曲线在某时间段内的关键帧
|
||||
"""
|
||||
if start > end:
|
||||
return
|
||||
start_index, starter_keyframe = self.find_key(start)
|
||||
end_index, ender_keyframe = self.find_key(end)
|
||||
if ender_keyframe:
|
||||
del self._keys[start_index : end_index + 1]
|
||||
else:
|
||||
del self._keys[start_index:end_index]
|
||||
|
||||
def add_key(
|
||||
self,
|
||||
time: float,
|
||||
value: float,
|
||||
out_interp: Optional[FittingFunctionType] = None,
|
||||
out_interp: Optional[Callable[[float], float]] = None,
|
||||
in_tangent: Optional[Tuple[float, float]] = None,
|
||||
out_tangent: Optional[Tuple[float, float]] = None,
|
||||
use_bezier: bool = False,
|
||||
@@ -270,7 +521,7 @@ class ParamCurve:
|
||||
time : float
|
||||
关键帧时间。
|
||||
value : float
|
||||
参数值。
|
||||
参数值(将自动受值域限制约束)。
|
||||
out_interp : Optional[Callable]
|
||||
出插值函数(若 use_bezier=False)。
|
||||
in_tangent : Optional[Tuple[float, float]]
|
||||
@@ -287,14 +538,18 @@ class ParamCurve:
|
||||
Notes
|
||||
-----
|
||||
若时间已存在,更新该关键帧的所有属性。
|
||||
写入的 value 会被自动 clamp 到 [value_min, value_max]。
|
||||
"""
|
||||
interp = (
|
||||
out_interp if out_interp is not None else self.base_interpolation_function
|
||||
)
|
||||
new_key = Keyframe(time, value, interp, in_tangent, out_tangent, use_bezier)
|
||||
clamped_value = self._clamp_value(value)
|
||||
new_key = Keyframe(
|
||||
time, clamped_value, interp, in_tangent, out_tangent, use_bezier
|
||||
)
|
||||
|
||||
idx = bisect.bisect_left(self._keys, time, key=lambda k: k.time)
|
||||
if idx < len(self._keys) and self._keys[idx].time == time:
|
||||
idx, old_key = self.find_key(time)
|
||||
if old_key:
|
||||
self._keys[idx] = new_key
|
||||
else:
|
||||
self._keys.insert(idx, new_key)
|
||||
@@ -312,33 +567,37 @@ class ParamCurve:
|
||||
-------
|
||||
None
|
||||
"""
|
||||
idx = bisect.bisect_left(self._keys, time, key=lambda k: k.time)
|
||||
if idx < len(self._keys) and self._keys[idx].time == time:
|
||||
idx, key = self.find_key(time)
|
||||
if key:
|
||||
del self._keys[idx]
|
||||
|
||||
def update_key_value(self, time: float, new_value: float):
|
||||
"""更新关键帧值,保留其他属性。"""
|
||||
idx = bisect.bisect_left(self._keys, time, key=lambda k: k.time)
|
||||
if idx < len(self._keys) and self._keys[idx].time == time:
|
||||
k = self._keys[idx]
|
||||
"""更新关键帧值,保留其他属性。值受值域限制约束。"""
|
||||
idx, key = self.find_key(time)
|
||||
if key:
|
||||
clamped_value = self._clamp_value(new_value)
|
||||
self._keys[idx] = Keyframe(
|
||||
time, new_value, k.out_interp, k.in_tangent, k.out_tangent, k.use_bezier
|
||||
time,
|
||||
clamped_value,
|
||||
key.out_interp,
|
||||
key.in_tangent,
|
||||
key.out_tangent,
|
||||
key.use_bezier,
|
||||
)
|
||||
|
||||
def update_key_interp(
|
||||
self,
|
||||
time: float,
|
||||
out_interp: Optional[FittingFunctionType] = None,
|
||||
out_interp: Optional[Callable[[float], float]] = None,
|
||||
in_tangent: Optional[Tuple[float, float]] = None,
|
||||
out_tangent: Optional[Tuple[float, float]] = None,
|
||||
use_bezier: bool = False,
|
||||
):
|
||||
"""更新关键帧的插值属性。"""
|
||||
idx = bisect.bisect_left(self._keys, time, key=lambda k: k.time)
|
||||
if idx < len(self._keys) and self._keys[idx].time == time:
|
||||
k = self._keys[idx]
|
||||
new_value = k.value
|
||||
interp = out_interp if out_interp is not None else k.out_interp
|
||||
idx, key = self.find_key(time)
|
||||
if key:
|
||||
new_value = key.value
|
||||
interp = out_interp if out_interp is not None else key.out_interp
|
||||
self._keys[idx] = Keyframe(
|
||||
time, new_value, interp, in_tangent, out_tangent, use_bezier
|
||||
)
|
||||
@@ -351,13 +610,12 @@ class ParamCurve:
|
||||
use_bezier: bool = True,
|
||||
):
|
||||
"""单独设置关键帧的切线,不改变值。"""
|
||||
idx = bisect.bisect_left(self._keys, time, key=lambda k: k.time)
|
||||
if idx < len(self._keys) and self._keys[idx].time == time:
|
||||
k = self._keys[idx]
|
||||
idx, key = self.find_key(time)
|
||||
if key:
|
||||
self._keys[idx] = Keyframe(
|
||||
time,
|
||||
k.value,
|
||||
out_interp=k.out_interp,
|
||||
key.value,
|
||||
out_interp=key.out_interp,
|
||||
in_tangent=in_tangent,
|
||||
out_tangent=out_tangent,
|
||||
use_bezier=use_bezier,
|
||||
@@ -365,12 +623,11 @@ class ParamCurve:
|
||||
|
||||
def make_key_smooth(self, time: float):
|
||||
"""
|
||||
将关键帧设为“平滑”模式(自动对称切线,并设为贝塞尔模式)。
|
||||
将关键帧设为"平滑"模式(自动对称切线,并设为贝塞尔模式)。
|
||||
切线长度基于相邻关键帧的时间和值差。
|
||||
"""
|
||||
idx = bisect.bisect_left(self._keys, time, key=lambda k: k.time)
|
||||
if idx < len(self._keys) and self._keys[idx].time == time:
|
||||
k = self._keys[idx]
|
||||
idx, key = self.find_key(time)
|
||||
if key:
|
||||
prev_k = self._keys[idx - 1] if idx > 0 else None
|
||||
next_k = self._keys[idx + 1] if idx + 1 < len(self._keys) else None
|
||||
|
||||
@@ -384,13 +641,13 @@ class ParamCurve:
|
||||
dt_in = dt_out = dt_total / 3.0
|
||||
dv_in = dv_out = dv_total / 3.0
|
||||
elif prev_k:
|
||||
dt_out = (k.time - prev_k.time) / 2.0
|
||||
dv_out = (k.value - prev_k.value) / 2.0
|
||||
dt_out = (key.time - prev_k.time) / 2.0
|
||||
dv_out = (key.value - prev_k.value) / 2.0
|
||||
dt_in = dt_out
|
||||
dv_in = dv_out
|
||||
elif next_k:
|
||||
dt_in = (next_k.time - k.time) / 2.0
|
||||
dv_in = (next_k.value - k.value) / 2.0
|
||||
dt_in = (next_k.time - key.time) / 2.0
|
||||
dv_in = (next_k.value - key.value) / 2.0
|
||||
dt_out = dt_in
|
||||
dv_out = dv_in
|
||||
|
||||
@@ -419,6 +676,9 @@ class ParamCurve:
|
||||
"""
|
||||
计算时间 t 处的曲线值。
|
||||
|
||||
返回值始终受 [value_min, value_max] 值域限制约束。
|
||||
即使贝塞尔插值产生过冲(overshoot),输出也会被 clamp。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
t : float
|
||||
@@ -427,57 +687,61 @@ class ParamCurve:
|
||||
Returns
|
||||
-------
|
||||
float
|
||||
插值结果。
|
||||
插值结果(已 clamp)。
|
||||
"""
|
||||
keys = self._keys
|
||||
if not keys:
|
||||
return self._get_boundary_value(t)
|
||||
raw = self._get_boundary_value(t)
|
||||
else:
|
||||
if t < keys[0].time or t > keys[-1].time:
|
||||
raw = self._get_boundary_value(t)
|
||||
else:
|
||||
times = [k.time for k in keys]
|
||||
idx = bisect.bisect_right(times, t) - 1
|
||||
|
||||
if t < keys[0].time or t > keys[-1].time:
|
||||
return self._get_boundary_value(t)
|
||||
if idx < 0:
|
||||
raw = self._get_boundary_value(t)
|
||||
elif idx >= len(keys) - 1:
|
||||
raw = keys[-1].value
|
||||
else:
|
||||
|
||||
times = [k.time for k in keys]
|
||||
idx = bisect.bisect_right(times, t) - 1
|
||||
k0 = keys[idx]
|
||||
k1 = keys[idx + 1]
|
||||
|
||||
if idx < 0:
|
||||
return self._get_boundary_value(t)
|
||||
if idx >= len(keys) - 1:
|
||||
return keys[-1].value
|
||||
if k0.time == k1.time:
|
||||
raw = k0.value
|
||||
elif k0.time == t:
|
||||
raw = k0.value
|
||||
elif k1.time == t:
|
||||
raw = k1.value
|
||||
else:
|
||||
t0, v0 = k0.time, k0.value
|
||||
t1, v1 = k1.time, k1.value
|
||||
u = (t - t0) / (t1 - t0)
|
||||
u = max(0.0, min(1.0, u))
|
||||
|
||||
k0 = keys[idx]
|
||||
k1 = keys[idx + 1]
|
||||
# 贝塞尔模式(高优先级)
|
||||
if k0.use_bezier or k1.use_bezier:
|
||||
raw = _evaluate_bezier_segment(
|
||||
t0,
|
||||
v0,
|
||||
t1,
|
||||
v1,
|
||||
out_tangent=k0.out_tangent,
|
||||
in_tangent=k1.in_tangent,
|
||||
u=u,
|
||||
)
|
||||
# 函数插值模式,优先处理阶梯保持模式
|
||||
elif k0.out_interp is InterpolationMethod.hold:
|
||||
raw = v0
|
||||
else:
|
||||
interp_func = (
|
||||
k0.out_interp or self.base_interpolation_function
|
||||
)
|
||||
v_norm = interp_func(u)
|
||||
raw = v0 + v_norm * (v1 - v0)
|
||||
|
||||
if k0.time == k1.time:
|
||||
return k0.value
|
||||
if k0.time == t:
|
||||
return k0.value
|
||||
if k1.time == t:
|
||||
return k1.value
|
||||
|
||||
t0, v0 = k0.time, k0.value
|
||||
t1, v1 = k1.time, k1.value
|
||||
u = (t - t0) / (t1 - t0)
|
||||
u = max(0.0, min(1.0, u))
|
||||
|
||||
# 贝塞尔模式(高优先级)
|
||||
if k0.use_bezier or k1.use_bezier:
|
||||
return _evaluate_bezier_segment(
|
||||
t0,
|
||||
v0,
|
||||
t1,
|
||||
v1,
|
||||
out_tangent=k0.out_tangent,
|
||||
in_tangent=k1.in_tangent, # ← 关键:使用下一帧的 in_tangent!
|
||||
u=u,
|
||||
)
|
||||
|
||||
# 函数插值模式,优先处理阶梯保持模式
|
||||
elif k0.out_interp is InterpolationMethod.hold:
|
||||
return v0
|
||||
|
||||
interp_func = k0.out_interp or self.base_interpolation_function
|
||||
v_norm = interp_func(u)
|
||||
return v0 + v_norm * (v1 - v0)
|
||||
return self._clamp_value(raw)
|
||||
|
||||
def __call__(self, t: float) -> float:
|
||||
return self.value_at(t)
|
||||
@@ -486,7 +750,7 @@ class ParamCurve:
|
||||
"""返回 (time, value) 列表。"""
|
||||
return [(k.time, k.value) for k in self._keys]
|
||||
|
||||
def set_default_interpolation_function(self, interp_func: FittingFunctionType):
|
||||
def set_default_interpolation_function(self, interp_func: Callable[[float], float]):
|
||||
"""设置默认插值函数。"""
|
||||
self.base_interpolation_function = interp_func
|
||||
|
||||
@@ -501,11 +765,11 @@ class ParamCurve:
|
||||
mode : BoundaryBehaviour
|
||||
边界行为设定
|
||||
base_value : Optional[float]
|
||||
当 mode=BoundaryBehaviour.CONSTANT 时,指定新的默认值。
|
||||
当 mode=BoundaryBehaviour.CONSTANT 时,指定新的默认值(受值域约束)。
|
||||
"""
|
||||
self.boundary_behaviour = mode
|
||||
if base_value is not None:
|
||||
self.base_line = base_value
|
||||
self.base_line = self._clamp_value(base_value)
|
||||
|
||||
def bake(
|
||||
self,
|
||||
@@ -514,41 +778,12 @@ class ParamCurve:
|
||||
sample_rate: Optional[float] = None,
|
||||
num_samples: Optional[int] = None,
|
||||
dtype: Any = None,
|
||||
) -> "np.ndarray": # type: ignore 这里这样用会报错吗?不知道,但是人工智能这样写了都,大抵是能用的吧
|
||||
) -> "np.ndarray": # type: ignore
|
||||
"""
|
||||
将参数曲线在指定时间范围内烘焙为 NumPy 数组,用于高性能实时查询或音频渲染。
|
||||
将参数曲线在指定时间范围内烘焙为 NumPy 数组。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
start : float
|
||||
烘焙起始时间(包含)。
|
||||
end : float
|
||||
烘焙结束时间(不包含)。
|
||||
sample_rate : Optional[float]
|
||||
采样率(单位:样本/时间单位)。例如,若时间单位为秒,sample_rate=48000 表示每秒 48k 样本。
|
||||
必须与 `num_samples` 二选一提供。
|
||||
num_samples : Optional[int]
|
||||
输出数组的总样本数。若提供,则忽略 `sample_rate`。
|
||||
dtype : Any, optional
|
||||
输出数组的数据类型(如 np.float32)。默认为 np.float64。
|
||||
|
||||
Returns
|
||||
-------
|
||||
np.ndarray
|
||||
一维 NumPy 数组,长度为 `num_samples`,`arr[i] ≈ curve(start + i / sample_rate)`。
|
||||
|
||||
Exceptions
|
||||
----------
|
||||
ValueError
|
||||
- 若 `start >= end`
|
||||
- 若未提供 `sample_rate` 且未提供 `num_samples`
|
||||
- 若 `num_samples <= 0`
|
||||
|
||||
Notes
|
||||
-----
|
||||
- 内部使用 `np.linspace` 生成时间轴,然后逐点调用 `self.value_at(t)`。
|
||||
- 虽然目前是 Python 循环,但对于典型自动化曲线(<1000 关键帧),NumPy 向量化优势主要体现在内存布局和后续处理。
|
||||
- 如需极致性能(如 >1M 样本),可未来优化为 C++/Numba 加速,但当前已满足 DAW 自动化需求。
|
||||
当值域限制已启用时,使用 np.clip 进行向量化 clamp,
|
||||
避免逐样本调用 Python 层的 clamp 逻辑。
|
||||
"""
|
||||
if start >= end:
|
||||
raise ValueError("起始值须小于结束值。")
|
||||
@@ -580,4 +815,9 @@ class ParamCurve:
|
||||
for i in range(n):
|
||||
values[i] = self.value_at(float(times[i]))
|
||||
|
||||
# 向量化 clamp —— 比逐点 Python 调用快一个数量级
|
||||
vmin, vmax = self.value_min, self.value_max
|
||||
if vmin is not None or vmax is not None:
|
||||
np.clip(values, vmin, vmax, out=values)
|
||||
|
||||
return values
|
||||
|
||||
+49
-23
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
存储 音·创 v3 的插件接口与管理相关内容
|
||||
音·创 v3 的插件接口与管理相关内容
|
||||
"""
|
||||
|
||||
"""
|
||||
@@ -33,6 +33,7 @@ from typing import (
|
||||
TypeVar,
|
||||
Mapping,
|
||||
Callable,
|
||||
Type,
|
||||
)
|
||||
from itertools import chain
|
||||
|
||||
@@ -60,9 +61,10 @@ from .exceptions import (
|
||||
ParameterTypeError,
|
||||
PluginInstanceNotFoundError,
|
||||
PluginRegisteredError,
|
||||
PluginNotFoundError,
|
||||
PluginDependencyNotFound,
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
# 枚举类
|
||||
"PluginTypes",
|
||||
@@ -110,7 +112,7 @@ T_Plugin = TypeVar(
|
||||
)
|
||||
|
||||
|
||||
def load_plugin_module(package: Union[Path, str]):
|
||||
def load_plugin_from_module(package: Union[Path, str]):
|
||||
"""自动发现并加载插件包中的插件
|
||||
|
||||
参数:
|
||||
@@ -119,16 +121,21 @@ def load_plugin_module(package: Union[Path, str]):
|
||||
插件包路径或名称,当为 Path 类时为路径,为 str 时为包名,切勿混淆。
|
||||
"""
|
||||
|
||||
if isinstance(package, Path):
|
||||
relative_path = package.resolve().relative_to(Path.cwd().resolve())
|
||||
if relative_path.stem == "__init__":
|
||||
return importlib.import_module(".".join(relative_path.parts[:-1]))
|
||||
try:
|
||||
if isinstance(package, Path):
|
||||
relative_path = package.resolve().relative_to(Path.cwd().resolve())
|
||||
if relative_path.stem == "__init__":
|
||||
return importlib.import_module(".".join(relative_path.parts[:-1]))
|
||||
else:
|
||||
return importlib.import_module(
|
||||
".".join(relative_path.parts[:-1] + (relative_path.stem,))
|
||||
)
|
||||
else:
|
||||
return importlib.import_module(
|
||||
".".join(relative_path.parts[:-1] + (relative_path.stem,))
|
||||
)
|
||||
else:
|
||||
return importlib.import_module(package)
|
||||
return importlib.import_module(package)
|
||||
except ModuleNotFoundError as e:
|
||||
raise PluginNotFoundError(
|
||||
"无法找到名为`{}`的插件包,或该插件包的依赖未正确安装。".format(package)
|
||||
) from e
|
||||
|
||||
|
||||
class PluginRegistry:
|
||||
@@ -143,6 +150,7 @@ class PluginRegistry:
|
||||
self._track_output_plugins: Dict[str, TrackOutputPluginBase] = {}
|
||||
self._service_plugins: Dict[str, ServicePluginBase] = {}
|
||||
self._library_plugins: Dict[str, LibraryPluginBase] = {}
|
||||
self._all_plugin_id: List = []
|
||||
|
||||
def __iter__(self) -> Iterator[Tuple[PluginTypes, Mapping[str, TopPluginBase]]]:
|
||||
"""迭代器,返回所有插件"""
|
||||
@@ -159,23 +167,38 @@ class PluginRegistry:
|
||||
)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _register_plugin(cls_dict: dict, plg_class: type, plg_id: str) -> None:
|
||||
def _register_plugin(
|
||||
self,
|
||||
cls_dict: Dict,
|
||||
plg_class: Type[TopPluginBase],
|
||||
plg_id: str,
|
||||
) -> None:
|
||||
"""注册插件"""
|
||||
if plg_id in cls_dict:
|
||||
if cls_dict[plg_id].metainfo.version >= plg_class.metainfo.version:
|
||||
raise PluginRegisteredError(
|
||||
"插件惟一识别码`{}`所对应的插件已存在更高版本`{}`,请勿重复注册同一插件!".format(
|
||||
"插件惟一识别码`{}`所对应的插件已存在更高版本`{}`,请勿重复注册同一插件。".format(
|
||||
plg_id, plg_class.metainfo
|
||||
)
|
||||
)
|
||||
if not cls_dict[plg_id].metainfo.similar_to(plg_class.metainfo):
|
||||
raise PluginRegisteredError(
|
||||
"插件惟一识别码`{}`已经存在插件了,请勿使用相同识别码。".format(
|
||||
plg_id
|
||||
)
|
||||
)
|
||||
if missing_requirements := [
|
||||
i for i in plg_class.metainfo.dependencies if i not in self._all_plugin_id
|
||||
]:
|
||||
raise PluginDependencyNotFound(
|
||||
"插件 `{}` 依赖于这些插件:`{}`;当前环境中缺失:`{}`。加载此插件时,请务必将被依赖的插件提前载入。".format(
|
||||
plg_id, plg_class.metainfo.dependencies, missing_requirements
|
||||
)
|
||||
)
|
||||
cls_dict[plg_id] = plg_class()
|
||||
self._all_plugin_id.append(plg_id)
|
||||
|
||||
def register_music_input_plugin(
|
||||
self,
|
||||
plugin_class: type,
|
||||
plugin_id: str,
|
||||
) -> None:
|
||||
def register_music_input_plugin(self, plugin_class: type, plugin_id: str) -> None:
|
||||
"""注册输入插件-整首曲目"""
|
||||
self._register_plugin(self._music_input_plugins, plugin_class, plugin_id)
|
||||
|
||||
@@ -209,7 +232,7 @@ class PluginRegistry:
|
||||
|
||||
@staticmethod
|
||||
def _get_io_plugin_by_format(
|
||||
plugin_regdict: Dict[str, T_IOPlugin], fpath_or_format: Union[Path, str]
|
||||
plugin_regdict: Mapping[str, T_IOPlugin], fpath_or_format: Union[Path, str]
|
||||
) -> Generator[T_IOPlugin, None, None]:
|
||||
if isinstance(fpath_or_format, str):
|
||||
return (
|
||||
@@ -218,6 +241,7 @@ class PluginRegistry:
|
||||
if plugin.can_handle_format(fpath_or_format)
|
||||
)
|
||||
elif isinstance(fpath_or_format, Path):
|
||||
# print("在",plugin_regdict,"中,查找可用于处理",fpath_or_format,"的插件")
|
||||
return (
|
||||
plugin
|
||||
for plugin in plugin_regdict.values()
|
||||
@@ -278,10 +302,10 @@ class PluginRegistry:
|
||||
],
|
||||
key=lambda plugin: plugin.metainfo.version,
|
||||
)
|
||||
except ValueError:
|
||||
except ValueError as e:
|
||||
raise PluginInstanceNotFoundError(
|
||||
"未找到“用于{}、名为`{}`”的插件".format(plugin_usage, plugin_name)
|
||||
)
|
||||
) from e
|
||||
|
||||
def get_music_input_plugin(self, plugin_name: str) -> MusicInputPluginBase:
|
||||
"""获取指定名称的全曲导入用插件,当名称重叠时,取版本号最大的"""
|
||||
@@ -389,6 +413,7 @@ def music_operate_plugin(plugin_id: str):
|
||||
plugin_id, _global_plugin_registry.register_music_operate_plugin
|
||||
)
|
||||
|
||||
|
||||
def track_operate_plugin(plugin_id: str):
|
||||
"""音轨处理插件装饰器"""
|
||||
return __plugin_regist_decorator(
|
||||
@@ -416,6 +441,7 @@ def service_plugin(plugin_id: str):
|
||||
plugin_id, _global_plugin_registry.register_service_plugin
|
||||
)
|
||||
|
||||
|
||||
def library_plugin(plugin_id: str):
|
||||
"""支持库插件装饰器"""
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
存储 音·创 v3 定义的一些数据类型,可以用于类型检查器
|
||||
音·创 v3 定义的一些数据类型,可以用于类型检查器
|
||||
"""
|
||||
|
||||
"""
|
||||
@@ -19,7 +19,3 @@ Terms & Conditions: License.md in the root directory
|
||||
|
||||
from typing import Callable, Dict, List, Literal, Mapping, Tuple, Union
|
||||
|
||||
FittingFunctionType = Callable[[float], float]
|
||||
"""
|
||||
拟合函数类型
|
||||
"""
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
[Bilibili: 金羿ELS]: https://img.shields.io/badge/Bilibili-%E9%87%91%E7%BE%BFELS-00A1E7?style=for-the-badge
|
||||
[Bilibili: 玉衡Alioth]: https://img.shields.io/badge/Bilibili-%E7%8E%89%E8%A1%A1Alioth-00A1E7?style=for-the-badge
|
||||
[CodeStyle: black]: https://img.shields.io/badge/code%20style-black-121110.svg?style=for-the-badge
|
||||
<!-- [Bilibili: 金羿ELS]: https://img.shields.io/badge/Bilibili-%E9%87%91%E7%BE%BFELS-00A1E7?style=for-the-badge&label=作者B站
|
||||
[Bilibili: 玉衡Alioth]: https://img.shields.io/badge/Bilibili-%E7%8E%89%E8%A1%A1Alioth-00A1E7?style=for-the-badge&label=作者B站 -->
|
||||
[CodeStyle: black]: https://img.shields.io/badge/code%20style-black-121110.svg?style=for-the-badge&label=代码风格
|
||||
[python]: https://img.shields.io/badge/python-3.8-AB70FF?style=for-the-badge
|
||||
[release]: https://img.shields.io/github/v/release/EillesWan/Musicreater?style=for-the-badge
|
||||
[license]: https://img.shields.io/badge/Licence-%E6%B1%89%E9%92%B0%E5%BE%8B%E8%AE%B8%E5%8F%AF%E5%8D%8F%E8%AE%AE-228B22?style=for-the-badge
|
||||
[release]: https://img.shields.io/github/v/release/TriM-Organization/Musicreater?style=for-the-badge&label=发行版
|
||||
[license]: https://img.shields.io/badge/Licence-%E6%B1%89%E9%92%B0%E5%BE%8B%E8%AE%B8%E5%8F%AF%E5%8D%8F%E8%AE%AE-228B22?style=for-the-badge&label=协议
|
||||
[commit-activity]: https://img.shields.io/github/commit-activity/m/TriM-Organization/Musicreater%2Fmaster?style=for-the-badge&label=提交活动&color=AB70FF
|
||||
|
||||
|
||||
|
||||
<h1 align="center">音·创 Musicreater </h1>
|
||||
|
||||
@@ -12,7 +15,7 @@
|
||||
</img>
|
||||
</p>
|
||||
|
||||
<h3 align="center">一款免费开源的《我的世界》数字音频支持库。</h3>
|
||||
<h3 align="center">一款高性能、插件化的《我的世界》数字音频支持库。</h3>
|
||||
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/BUILD%20WITH%20LOVE-FF3432?style=for-the-badge">
|
||||
@@ -22,8 +25,8 @@
|
||||
</a>
|
||||
<p>
|
||||
|
||||
[![][Bilibili: 金羿ELS]](https://space.bilibili.com/397369002/)
|
||||
[![][Bilibili: 玉衡Alioth]](https://space.bilibili.com/604072474)
|
||||
<!-- [![][Bilibili: 金羿ELS]](https://space.bilibili.com/397369002/)
|
||||
[![][Bilibili: 玉衡Alioth]](https://space.bilibili.com/604072474) -->
|
||||
[![CodeStyle: black]](https://github.com/psf/black)
|
||||
[![][python]](https://www.python.org/)
|
||||
[![][license]](LICENSE)
|
||||
@@ -38,40 +41,58 @@
|
||||
|
||||
## 介绍 🚀
|
||||
|
||||
音·创 是一款免费开源的针对 **《我的世界》** 音乐的支持库
|
||||
音·创 是一款极致优化的针对 **《我的世界》** 音乐的支持库,通过插件化的设计,支持多种处理方式。
|
||||
|
||||
欢迎加群:[861684859](https://jq.qq.com/?_wv=1027&k=hpeRxrYr)
|
||||
|
||||
> **注意** 本仓库内的项目仅仅是支持库,其用户为基岩版音乐相关软件的开发者
|
||||
>
|
||||
> 面向常规用户的 **基岩版音乐转换工具** 请参阅:[伶伦转换器](../../../Linglun-Converter)
|
||||
>
|
||||
> **注意** 本仓库内的项目仅仅是支持库,其用户为基岩版音乐相关软件的开发者 \
|
||||
> 面向常规用户的 **基岩版音乐转换工具** 请参阅:[伶伦转换器](../../../Linglun-Converter)【已停止更新】 \
|
||||
> 我们也正在开发面向高级用户的 **基岩版音乐编辑工具**(数字音频工作站):[伶伦](../../../LinglunStudio)
|
||||
|
||||
|
||||
## 安装 🔳
|
||||
|
||||
- 使用 pypi
|
||||
> 理论适用版本:Python 3.8 \
|
||||
> 实际开发版本:Python 3.10
|
||||
|
||||
```bash
|
||||
使用 pypi 直接安装
|
||||
|
||||
- ```bash
|
||||
pip install --upgrade Musicreater
|
||||
```
|
||||
|
||||
- 如果无法更新最新,可以尝试:
|
||||
如果因第三方源同步较慢,无法更新至最新,可以尝试使用 pypi 的默认源:
|
||||
|
||||
```bash
|
||||
- ```bash
|
||||
pip install --upgrade -i https://pypi.python.org/simple Musicreater
|
||||
```
|
||||
|
||||
- 克隆仓库并安装(最新内容但**不推荐**)
|
||||
如果需要使用一些内置插件,则可以在库名称后面加上对应的可选依赖标签,现有的插件和可选依赖标签对应如下:
|
||||
|
||||
```bash
|
||||
git clone https://gitee.com/TriM-Organization/Musicreater.git
|
||||
cd Musicreater
|
||||
python setup.py install
|
||||
- | 内置插件 | 插件功能 | 可选依赖标签 | 说明 |
|
||||
| -------- | -------- | ------------ | ---- |
|
||||
| Midi 读取 | 读取 MIDI 文件,导入音轨或全曲 | `midi` | 未来可能支持 MIDI 写入 |
|
||||
| Minecraft 指令结构生成 | 导出乐曲,创建 Minecraft 指令方块存储的音乐结构 | `structure` | 当前支持 `.mcstructure`、`.bdx` 格式直接导出和 `.mcpack` 附加包格式的导出,具体导出格式请查阅[插件文档](./docs/内置插件相关/MC指令结构/) |
|
||||
| 音乐预览 | 以 Minecraft 音效直接输出音乐的波形声音文件 <hr> 导入自制的 Minecraft 音乐资源包并解析,并输出预览结果波形(实验性) | `preview` <hr> `preview-advanced` | 输出为 `.wav` 格式的音乐文件,用于预览音乐的播放效果 <hr> 需与 preview 标签一并安装 |
|
||||
|
||||
除了内置插件有对应的可选依赖标签外,还有另外两组可选依赖:
|
||||
|
||||
- | 标签 | 说明 |
|
||||
| ------------ | ---- |
|
||||
| `full` | 添加所有可选依赖,即包含所有插件的依赖项 |
|
||||
| `dev` | 添加开发依赖,包括一些开发或测试功能时所用插件、以及 `full` 标签中的所有插件的依赖项。 |
|
||||
|
||||
也就是说,如果需要使用 midi 读取和 Minecraft 指令结构生成,则需要安装 `midi` 和 `structure` 标签所对应的依赖项。例如:
|
||||
|
||||
- ```bash
|
||||
pip install "Musicreater[midi,structure]"
|
||||
```
|
||||
|
||||
以上命令中 `python`、`pip` 请依照各个环境不同灵活更换,可能为`python3`或`pip3`之类。
|
||||
参与开发,可以采用最新的开发分支下的滚动更新:
|
||||
|
||||
- ```bash
|
||||
pip install "git+https://gitee.com/TriM-Organization/Musicreater.git@develop[dev]"
|
||||
```
|
||||
|
||||
## 文档 📄
|
||||
|
||||
@@ -81,20 +102,30 @@
|
||||
|
||||
## 作者 ✒
|
||||
|
||||
**金羿 Eilles**:我的世界基岩版指令作者,个人开发者,B 站不知名 UP 主。
|
||||
以下人名依照对于各项的参与之序排布。
|
||||
|
||||
### 项目策划
|
||||
|
||||
**金羿 Eilles**、**玉衡Alioth Alioth**
|
||||
|
||||
### 软件开发
|
||||
|
||||
**金羿 Eilles**、**玉衡Alioth Alioth**、**鱼旧梦 ElapsingDreams**
|
||||
|
||||
### 资源素材
|
||||
|
||||
**Charlie_Ping “查理平”**、**偷吃不是Touch Touch**、**金羿 Eilles**
|
||||
|
||||
**玉衡Alioth Alioth**:我的世界基岩版玩家,喜欢编程和音乐,学生。
|
||||
|
||||
**偷吃不是Touch Touch**:我的世界基岩版指令制作者,提供测试支持
|
||||
|
||||
## 致谢 🙏
|
||||
|
||||
本致谢列表排名无顺序。
|
||||
本致谢列表按照其提交的内容生效之时间依次排序。
|
||||
|
||||
- 感谢 **昀梦**\<QQ1515399885\> 找出指令生成错误 bug 并指正
|
||||
- 感谢由 **Charlie_Ping “查理平”** 带来的 BDX 文件转换参考,以及 MIDI-我的世界对应乐器 参考表格
|
||||
- 感谢由 **[CMA_2401PT](https://github.com/CMA2401PT)** 为我们的软件开发的一些方面进行指导,同时我们参考了他的 BDXworkshop 作为 BDX 结构编辑的参考
|
||||
- 感谢由 **[Dislink Sforza](https://github.com/Dislink) “断联·斯福尔扎”**\<QQ1600515314\> 带来的 midi 音色解析以及转换指令的算法,我们将其改编并应用;同时,感谢他的[网页版转换器](https://dislink.github.io/midi2bdx/)给我们的开发与更新带来巨大的压力和动力,让我们在原本一骑绝尘的摸鱼道路上转向开发。
|
||||
- 感谢由 **[Dislink Sforza](https://github.com/Dislink) “断联·斯福尔扎”**\<QQ1600515314\> 的[网页版转换器](https://dislink.github.io/midi2bdx/)给我们的开发与更新带来巨大的压力和动力,让我们在原本一骑绝尘的摸鱼道路上转向开发。
|
||||
- 感谢 **Mono**\<QQ738893087\> 反馈安装时的问题,辅助我们找到了视窗操作系统下的兼容性问题;感谢其反馈延迟播放器出现的重大问题,让我们得以修改全部延迟播放错误;尤其感谢他对于我们的软件的大力宣传
|
||||
- 感谢 **Ammelia “艾米利亚”**\<QQ2838334637\> 敦促我们进行新的功能开发,并为新功能提出了非常优秀的大量建议,以及提供的 BDX 导入测试支持,为我们的新结构生成算法提供了大量的实际理论支持
|
||||
- 感谢 **[神羽 “SnowyKami”](https://www.sfkm.me/)** 对我们项目的支持与宣传,非常感谢他为我们提供的服务器!
|
||||
@@ -105,6 +136,7 @@
|
||||
- 感谢 **雨**\<QQ237667809\> 反馈在新版本的指令格式下,计分板播放器的附加包无法播放的问题。
|
||||
- 感谢 **梦幻duang**\<QQ13753593\> 为我们提供 Java 1.12.2 版本命令格式参考。
|
||||
- 感谢 [_Open Note Block Studio_](https://github.com/OpenNBS/NoteBlockStudio) 项目的开发为我们提供持续的追赶动力。
|
||||
- 感谢 **启航与凡凡**\<QQ2777856500\> 找到 **音·创 v2** 版本音符序列文件解码的错误并指出修正方式。
|
||||
|
||||
> 感谢广大群友为此库提供的测试和建议等
|
||||
> 若您对我们有所贡献但您的名字没有出现在此列表中,请联系我们!
|
||||
@@ -115,7 +147,11 @@
|
||||
|
||||
如果需要与开发组进行交流,欢迎加入我们的[开发闲聊 Q 群](https://jq.qq.com/?_wv=1027&k=hpeRxrYr)。
|
||||
|
||||
亦可以联系我们[睿乐组织官方邮箱](mailto:TriM-Organization@hotmail.com),取得进一步联系!
|
||||
亦可以联系我们组织的[邮箱](mailto:TriM-Organization@hotmail.com)!
|
||||
|
||||
## 待办 ⏰
|
||||
|
||||
当前未完成的任务和一些需要讨论的内容都收集在了[任务清单](./TO-DO.md)里,当然你也可以提[工单请求](https://gitee.com/TriM-Organization/Musicreater/issues/new)来进行讨论。
|
||||
|
||||
---
|
||||
|
||||
@@ -127,7 +163,7 @@
|
||||
|
||||
“Minecraft”是 Mojang Synergies AB 的商标,此项目中所有对于“我的世界”、“Minecraft”等相关称呼均为必要的介绍性使用
|
||||
|
||||
- 上文提及的 网易 公司,指代的是在中国大陆运营《我的世界:中国版》的上海网之易璀璨网络科技有限公司
|
||||
- 上文提及的 网易 公司,指代的是在中国大陆运营《我的世界:中国版》的上海网之易吾世界网络科技有限公司
|
||||
|
||||
NOT AN OFFICIAL MINECRAFT PRODUCT.
|
||||
|
||||
|
||||
+43
-40
@@ -14,7 +14,7 @@
|
||||
</img>
|
||||
</p>
|
||||
|
||||
<h3 align="center">A free and open-source library for handling with <i>Minecraft</i> digital music.</h3>
|
||||
<h3 align="center">A blazing fast and plugin-supported library for editing <i>Minecraft</i> digital music.</h3>
|
||||
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/BUILD%20WITH%20LOVE-FF3432?style=for-the-badge">
|
||||
@@ -35,40 +35,33 @@
|
||||
|
||||
[简体中文 🇨🇳](README.md) | English🇬🇧
|
||||
|
||||
**Notice that the localizations of documents may probably NOT be up-to-date. The original document is in Chinese.**
|
||||
> **NOTICE** The localizations of documents may probably NOT be up-to-date. The original document is in Chinese.
|
||||
|
||||
## Introduction🚀
|
||||
|
||||
Musicreater is a free open-source library used for handling with digital musics that being able to be played in _Minecraft_.
|
||||
Musicreater is a cost free, open-source, highly optimized library designed for editing digital musics that being able to be played in _Minecraft_. With the plugins, the library supports a lot of processing methods.
|
||||
|
||||
Welcome to join our QQ group: [861684859](https://jq.qq.com/?_wv=1027&k=hpeRxrYr)
|
||||
|
||||
> **NOTICE** The project inside this repository is only the support library, which is mainly for the developers of music related software for _Minecraft: Bedrock Edition_.
|
||||
>
|
||||
> The _Bedrock Edition music convertor_ which is for common users is in [Linglun Converter](../../../Linglun-Converter).
|
||||
>
|
||||
> We are also developing a _BE music editor_ (digital audio workstation): [Linglun](../../../LinglunStudio)
|
||||
> **NOTICE** The project inside this repository is only a supportive library, which is mainly for the developers of music related software for _Minecraft: Bedrock Edition_. \
|
||||
> The _Bedrock Edition music convertor_ which is for common users is in [Linglun Converter](../../../Linglun-Converter). [Archived] \
|
||||
> We are also developing a _BE music editor_, a new _Minecraft_ digital audio workstation(DAW): [Linglun](../../../LinglunStudio)
|
||||
|
||||
|
||||
## Installation 🔳
|
||||
|
||||
> Supported Version: Python 3.8 \
|
||||
> Development Under: Python 3.10
|
||||
|
||||
- Via pypi
|
||||
|
||||
```bash
|
||||
pip install Musicreater --upgrade
|
||||
```
|
||||
|
||||
- If above command cannot fetch latest version, try:
|
||||
|
||||
- Clone the letest version under development:
|
||||
```bash
|
||||
pip install -i https://pypi.python.org/simple Musicreater --upgrade
|
||||
```
|
||||
|
||||
- Clone repo and Install (Latest but **NOT RECOMMANDED**):
|
||||
```bash
|
||||
git clone https://github.com/TriM-Organization/Musicreater.git
|
||||
cd Musicreater
|
||||
python setup.py install
|
||||
pip install "git+https://github.com/TriM-Organization/Musicreater.git@develop[dev]"
|
||||
```
|
||||
|
||||
Commands such as `python`、`pip` could be changed to some like `python3` or `pip3` according to the difference of platforms.
|
||||
@@ -81,32 +74,42 @@ Commands such as `python`、`pip` could be changed to some like `python3` or `pi
|
||||
|
||||
[仓库 API 文档](./docs/%E5%BA%93%E7%9A%84%E7%94%9F%E6%88%90%E4%B8%8E%E5%8A%9F%E8%83%BD%E6%96%87%E6%A1%A3.md)
|
||||
|
||||
### Authors ✒
|
||||
## Authors ✒
|
||||
|
||||
**Eilles (金羿)**:A student, individual developer, unfamous Bilibili UPer, which knows a little about commands in _Minecraft: Bedrock Edition_
|
||||
The names are in the order of each person's first contributing time of each part of the project.
|
||||
|
||||
**Alioth (玉衡Alioth)**: A student, player of _Minecraft: Bedrock Edition_, which is a fan of music and programming.
|
||||
### Project Managers
|
||||
|
||||
**Eilles (菀 金羿)**, **Alioth (玉衡Alioth)**
|
||||
|
||||
### Developers
|
||||
|
||||
**Eilles (菀 金羿)**, **Alioth (玉衡Alioth)**, **ElapsingDreams (鱼旧梦)**
|
||||
|
||||
### Resource Providers
|
||||
|
||||
**Charlie_Ping**, **Touch (偷吃不是Touch)**, **Eilles (菀 金羿)**
|
||||
|
||||
**Touch (偷吃不是 Touch)**: A man who is good at using command(s) in _Minecraft: Bedrock Edition_, who supported us of debugging and testing program and algorithm
|
||||
|
||||
## Acknowledgements 🙏
|
||||
|
||||
This list is not in any order.
|
||||
We thank:
|
||||
|
||||
- Thank _昀梦_\<QQ1515399885\> for finding and correcting the bugs in the commands that _Musicreater_ generated.
|
||||
- Thank _Charlie_Ping “查理平”_ for the bdx convert function for reference, and the reference chart that's used to convert the mid's instruments into Minecraft's instruments.
|
||||
- Thank _[CMA_2401PT](https://github.com/CMA2401PT)_ for BDXWorkShop for reference of the .bdx structure's operation, and his guidance in some aspects of our development.
|
||||
- Thank _[Dislink Sforza](https://github.com/Dislink) “断联·斯福尔扎”_ \<QQ1600515314\> for his midi analysis algorithm brought to us, we had adapted it and made it applied in one of our working method; Also, thank him for the [WebConvertor](https://dislink.github.io/midi2bdx/) which brought us so much pressure and power to develop as well as update our projects better, instead of loaf on our project.
|
||||
- Thank _Mono_\<QQ738893087\> for reporting problems while installing
|
||||
- Thank _Ammelia “艾米利亚”_\<QQ2838334637\> for urging us to develop new functions, and put forward a lot of excellent suggestions for new functions, as well as the BDX file's importing test support provided, which has given a lot of practical theoretical support for our new Structure Generating Algorithm
|
||||
- Thank _[神羽](https://gitee.com/snowykami) “[SnowyKami](https://github.com/snowyfirefly)”_ for supporting and promoting our project, and also thanks him for his server which given us to use for free.
|
||||
- Thank _指令师\_苦力怕 “playjuice123”_\<QQ240667197\> for finding bugs within our code, and noticed us to repair a big problem.
|
||||
- Thank _雷霆_\<QQ3555268519\> for his annoying and provoking operations which may awake some problems within the program by chance and reminding us to repair.
|
||||
- Thank _小埋_\<QQ2039310975\> for reporting the empty add-on packs title and description problem.
|
||||
- <table><tr><td>Thank <i>油炸</i> <QQ2836146704> for inspiring us to constantly develop something new.</td><td><img width="260" src="https://foruda.gitee.com/images/1695478907647543027/08ea9909_9911226.jpeg" alt="The groupmate on the picture was saying that our convert-QQ-bot had once brought him great convinience but now it closed down by some reason so he was feeling regretful." title=""It was once, a convert-QQ-bot is just in front my eyes" "Until lose, I finally know cannot chase back what I needs""></td><td><small>"It was once, a convert-QQ-bot is just in front my eyes"<br>"Until lose, I finally know cannot chase back what I needs"</small></td></tr></table>
|
||||
- Thank _雨_\<QQ237667809\> for give us report that under the new `execute` command format that the scoreboard player's add-on packs cannot play correctly.
|
||||
- Thank _梦幻duang_\<QQ13753593\> for providing us with his knowlodeg of the command format in Minecraft: Java Edition Version 1.12.2.
|
||||
- Thank [_Open Note Block Studio_](https://github.com/OpenNBS/NoteBlockStudio)'s Project for giving us the power and energy of continual developing.
|
||||
- _昀梦_\<QQ1515399885\> for finding and correcting the bugs in the commands that _Musicreater_ generated.
|
||||
- _Charlie_Ping “查理平”_ for the bdx convert function for reference, and the reference chart that's used to convert the mid's instruments into Minecraft's instruments.
|
||||
- _[CMA_2401PT](https://github.com/CMA2401PT)_ for BDXWorkShop for reference of the .bdx structure's operation, and his guidance in some aspects of our development.
|
||||
- _[Dislink Sforza](https://github.com/Dislink) “断联·斯福尔扎”_ \<QQ1600515314\> for the [WebConvertor](https://dislink.github.io/midi2bdx/) which brought us so much pressure and power to develop as well as update our projects better, instead of loaf on our project.
|
||||
- _Mono_\<QQ738893087\> for reporting problems while installing
|
||||
- _Ammelia “艾米利亚”_\<QQ2838334637\> for urging us to develop new functions, and put forward a lot of excellent suggestions for new functions, as well as the BDX file's importing test support provided, which has given a lot of practical theoretical support for our new Structure Generating Algorithm
|
||||
- _[神羽](https://gitee.com/snowykami) “[SnowyKami](https://github.com/snowyfirefly)”_ for supporting and promoting our project, and also thanks him for his server which given us to use for free.
|
||||
- _指令师\_苦力怕 “playjuice123”_\<QQ240667197\> for finding bugs within our code, and noticed us to repair a big problem.
|
||||
- _雷霆_\<QQ3555268519\> for his annoying and provoking operations which may awake some problems within the program by chance and reminding us to repair.
|
||||
- _小埋_\<QQ2039310975\> for reporting the empty add-on packs title and description problem.
|
||||
- <table><tr><td><i>油炸</i> <QQ2836146704> for inspiring us to constantly develop something new.</td><td><img width="260" src="https://foruda.gitee.com/images/1695478907647543027/08ea9909_9911226.jpeg" alt="The groupmate on the picture was saying that our convert-QQ-bot had once brought him great convinience but now it closed down by some reason so he was feeling regretful." title=""It was once, a convert-QQ-bot is just in front my eyes" "Until lose, I finally know cannot chase back what I needs""></td><td><small>"It was once, a convert-QQ-bot is just in front my eyes"<br>"Until lose, I finally know cannot chase back what I needs"</small></td></tr></table>
|
||||
- _雨_\<QQ237667809\> for give us report that under the new `execute` command format that the scoreboard player's add-on packs cannot play correctly.
|
||||
- _梦幻duang_\<QQ13753593\> for providing us with his knowlodeg of the command format in Minecraft: Java Edition Version 1.12.2.
|
||||
- the project of [_Open Note Block Studio_](https://github.com/OpenNBS/NoteBlockStudio) for giving us the power and energy of continual developing.
|
||||
- _启航与凡凡_\<QQ2777856500\> for pointing out the decoding error of _Musicreater v2_'s music sequence file, and the way to fix.
|
||||
|
||||
> Thanks for the support and help of a lot of groupmates
|
||||
> If you have given contributions but have not been in the list, please contact us!
|
||||
@@ -117,7 +120,7 @@ Meet problems? Welcome to give out your issue [here](../../issues/new)!
|
||||
|
||||
Want to get in contact of developers? Welcome to join our [Chat QQ group](https://jq.qq.com/?_wv=1027&k=hpeRxrYr).
|
||||
|
||||
Or contact us via [TriM-Org Official Email](mailto:TriM-Organization@hotmail.com)!
|
||||
Or contact us via [TriM-Org Email](mailto:TriM-Organization@hotmail.com)!
|
||||
|
||||
---
|
||||
|
||||
@@ -131,8 +134,8 @@ NOT APPROVED BY OR ASSOCIATED WITH NETEASE.
|
||||
|
||||
此项目不隶属或关联于 Mojang Studios 或 微软
|
||||
|
||||
此项目亦不隶属或关联于 网易 相关
|
||||
此项目亦不隶属或关联于 网易
|
||||
|
||||
“Minecraft”是 Mojang Synergies AB 的商标,此项目中所有对于“我的世界”、“Minecraft”等相关称呼均为必要的介绍性使用
|
||||
|
||||
- 上文提及的 网易 公司,指代的是在中国大陆运营《我的世界:中国版》的上海网之易璀璨网络科技有限公司
|
||||
- 上文提及的 网易 公司,指代的是在中国大陆运营《我的世界:中国版》的上海网之易吾世界网络科技有限公司
|
||||
|
||||
@@ -1,48 +1,65 @@
|
||||
# 任务清单
|
||||
|
||||
## 待办事项
|
||||
- 乐曲文件格式设计
|
||||
- - [ ] 乐曲文件格式设计
|
||||
目前想到的是:
|
||||
1. 使用 `.MCT` 作为项目文件的后缀,然后考虑一下格式是否和之前的 MusicSequence 兼容,如果兼容的话可以照旧用 `.MSQ`,如不的话,可以试试想一个新的后缀名作为数据文件后缀
|
||||
2. 要求数据文件支持完全流式读入
|
||||
|
||||
- 音轨静音处理
|
||||
- - [ ] 音轨静音处理
|
||||
当前没有处理
|
||||
|
||||
- 优化音轨的存储方式
|
||||
- - [ ] 优化音轨的存储方式
|
||||
当前是用列表,且每一次变动元素都要重新排序,这样消耗太大了,需要优化,改用最小堆形式(heapq)
|
||||
|
||||
- 移植 v2 功能到内置插件
|
||||
目前 v2 的功能有很多,都要移植到 v3。
|
||||
1. 导入 Midi 文件到全曲
|
||||
2. 导入 Midi 文件到指定轨道
|
||||
3. 导出到延迟播放器的结构文件(MCSTRUCTURE、BDX)
|
||||
4. 导出到延迟播放器的附加包
|
||||
5. 导出到积分板播放器的以上两种形式
|
||||
6. 导出到中继器播放器的以上两种形式
|
||||
7. 在 WebSocket 播放器中播放
|
||||
8. 导出到支持神羽资源包的以上 7 种形式
|
||||
9. 对于 Midi 歌词的实验性功能
|
||||
10. 对于 Java 版本适配的实验性功能
|
||||
11. 对于听感优化的实验性功能(插值、偏移)
|
||||
1. - [x] 导入 Midi 文件到全曲
|
||||
2. - [ ] 导入 Midi 文件到指定轨道
|
||||
3. - [x] 导出到延迟播放器的结构文件(MCSTRUCTURE、BDX)
|
||||
4. - [ ] 导出到延迟播放器的附加包
|
||||
5. - [ ] 导出到积分板播放器的_以上两种形式_(结构文件和附加包)
|
||||
6. - [ ] 导出到中继器播放器的以上两种形式
|
||||
7. - [ ] 在 WebSocket 播放器中播放
|
||||
8. - [ ] 导出到支持神羽资源包的以上 7 种形式
|
||||
9. - [ ] 对于 Midi 歌词的实验性功能
|
||||
10. - [ ] 对于 Java 版本适配的实验性功能
|
||||
11. - [ ] 对于听感优化的实验性功能(插值、偏移)
|
||||
|
||||
- 测试参数曲线的功能
|
||||
- - [ ] 测试参数曲线的功能
|
||||
是的,我们现在还没有测试参数曲线的功能。
|
||||
|
||||
- 支持导出音符盒构成的音乐
|
||||
- - 制作更多新的内置或附加插件
|
||||
1. - [ ] 支持导出音符盒构成的音乐
|
||||
2. - [ ] 支持导出成 schematic 结构
|
||||
3. - [ ] 导出到以瀑布流为主要形式的各种播放器
|
||||
3. - [x] 支持瀑布流式样的动画内容导出(详见[音·视](https://gitee.com/TriM-Organization/MineMusicVisualizer))
|
||||
4. - [ ] 在中继器播放器中,支持将指定玩家移动到对应播放位置
|
||||
5. - [x] 支持将音乐渲染成音频文件(详见[音·预](https://gitee.com/ElapsingDreams/MusicPreview))
|
||||
6. - [ ] 音符可视化的拓展,该内容可以考虑作为音·预以及音·视的子插件
|
||||
7. - [ ] 支持导入 Everyone Piano 的曲谱文件(.eop)
|
||||
8. - [ ] 支持导入 Musescore 的通用曲谱文件,即 musicXML(.mscz、.mscx)
|
||||
9. - [ ] 支持导入 JPword 曲谱文件(.jpd)
|
||||
10. - [ ] 支持识别曲谱图片以导入
|
||||
11. - [ ] 支持识别地图或者结构中利用中继器进行播放的音乐并导入
|
||||
|
||||
- 支持导出成 schematic 结构
|
||||
|
||||
## 讨论
|
||||
|
||||
1. [x] 是否应该在插件注册表 PluginRegistry 中采用 `Dict[插件名, 插件对象]` 的形式存储插件?
|
||||
1. - [x] 是否应该在插件注册表 PluginRegistry 中采用 `Dict[插件名, 插件对象]` 的形式存储插件?
|
||||
当前不采用这种方式是认为可以兼容一些极端场景下用户将一堆同名插件放在一起的情况。但是就算是插件放在一起,我们也可以有选择地读入注册表,比如依照版本号只读取最高版本的插件,并不需要全部存储在插件注册表中。所以其实用字典来存储是有利的?吗?
|
||||
|
||||
**当前已解决**
|
||||
|
||||
引入了插件惟一识别码之后当然是采用 `Dict[插件唯一识别码, 插件对象]` 来存储插件了~之前插件名称的内容是我想得太浅了,我写完所有代码之后才想到插件名称是中文还带空格的任意字符串……
|
||||
|
||||
2. 服务插件到底该怎么写?总不能留着一个 PluginType.SERVICE 的插件一直空在那里吧……
|
||||
2. - [ ] 服务插件到底该怎么写?总不能留着一个 PluginType.SERVICE 的插件一直空在那里吧……
|
||||
|
||||
3. 插件依赖性的优化。目前没有处理各个插件依赖关系的问题,如果插件之间彼此依赖要怎么做?
|
||||
3. - [x] 插件依赖性的优化。目前没有处理各个插件依赖关系的问题,如果插件之间彼此依赖要怎么做?
|
||||
我的想法是,这个依赖的处理由调用端来完成。比如我们的 伶伦工作站 是以 音·创 为核心的一个可视化数字音频工作站。
|
||||
那么应该由伶伦来处理依赖关系并加载之。
|
||||
|
||||
**当前已经大致解决**
|
||||
|
||||
首先有一个验证顺序,我们在插件加载后会验证,当前已加载的插件中是否包括了所需的插件,如果缺少则报错。
|
||||
这样的加载顺序安排仍然需要调用端来实现。
|
||||
|
||||
+1
-5
@@ -6,8 +6,4 @@
|
||||
|
||||
**此为开发相关文档,内容包括:库的简单调用、所生成文件结构的详细说明、特殊参数的详细解释**
|
||||
|
||||
# [main.py](../Musicreater/main.py)
|
||||
|
||||
## [类] MidiConvert
|
||||
|
||||
### [类函数] from_midi_file
|
||||
音·创 v3 的文档还在编纂过程中,请耐心等待。
|
||||
@@ -1,148 +1,15 @@
|
||||
<h1 align="center">音·创 Musicreater</h1>
|
||||
<h2 align="center">Minecraft 指令结构生成插件</h1>
|
||||
|
||||
|
||||
<p align="center">
|
||||
<img width="128" height="128" src="https://gitee.com/TriM-Organization/Musicreater/raw/master/resources/msctIcon.png" >
|
||||
<img width="128" height="128" src="../../../../raw/master/resources/msctIcon.png" >
|
||||
</p>
|
||||
|
||||
**此为开发相关文档,内容包括:库的简单调用、所生成文件结构的详细说明、特殊参数的详细解释**
|
||||
<h3 align="center">生成文件的结构</h3>
|
||||
|
||||
# 库的简单调用
|
||||
|
||||
参见[example.py的相关部分](../example.py),使用此库进行MIDI转换非常简单。
|
||||
|
||||
- 在导入转换库后,使用 MidiConvert 类建立转换对象(读取Midi文件)
|
||||
|
||||
音·创库支持新旧两种execute语法,需要在对象实例化时指定
|
||||
```python
|
||||
# 导入音·创库
|
||||
import Musicreater
|
||||
|
||||
# 指定是否使用旧的execute指令语法(即1.18及以前的《我的世界:基岩版》语法)
|
||||
old_execute_format = False
|
||||
|
||||
# 可以通过文件地址自动读取
|
||||
cvt_mid = Musicreater.MidiConvert.from_midi_file(
|
||||
"Midi文件地址",
|
||||
old_exe_format=old_execute_format
|
||||
)
|
||||
|
||||
# 也可以导入Mido对象
|
||||
cvt_mid = Musicreater.MidiConvert(
|
||||
mido.MidiFile("Midi文件地址"),
|
||||
"音乐名称",
|
||||
old_exe_format=old_execute_format
|
||||
)
|
||||
```
|
||||
|
||||
- 获取 Midi 音乐经转换后的播放指令
|
||||
|
||||
```python
|
||||
# 通过函数 to_command_list_in_score, to_command_list_in_delay
|
||||
# 分别可以得到
|
||||
# 以计分板作为播放器的指令对象列表、以延迟作为播放器的指令对象列表
|
||||
# 数据不仅存储在对象本身内,也会以返回值的形式返回,详见代码内文档
|
||||
|
||||
# 使用 to_command_list_in_score 函数进行转换之后,返回值有三个
|
||||
# 值得注意的是第一个返回值返回的是依照midi频道存储的指令对象列表
|
||||
# 也就是列表套列表
|
||||
# 但是,在对象内部所存储的数据却不会如此嵌套
|
||||
command_channel_list, command_count, max_score = cvt_mid.to_command_list_in_score(
|
||||
"计分板名称",
|
||||
1.0, # 音量比率
|
||||
1.0, # 速度倍率
|
||||
)
|
||||
|
||||
# 使用 to_command_list_in_delay 转换后的返回值只有两个
|
||||
# 但是第一个返回值没有列表套列表
|
||||
command_list, max_delay = cvt_mid.to_command_list_in_delay(
|
||||
1.0, # 音量比率
|
||||
1.0, # 速度倍率
|
||||
"@a", # 玩家选择器
|
||||
)
|
||||
|
||||
# 运行之后,指令和总延迟会存储至对象内
|
||||
print(
|
||||
"音乐长度:{}/游戏刻".format(
|
||||
cvt_mid.music_tick_num
|
||||
)
|
||||
)
|
||||
print(
|
||||
"指令如下:\n{}".format(
|
||||
cvt_mid.music_command_list
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
- 除了获取播放指令外,还可以获取进度条指令
|
||||
|
||||
```python
|
||||
# 通过函数 form_progress_bar 可以获得
|
||||
# 以计分板为载体所生成的进度条的指令对象列表
|
||||
# 数据不仅存储在对象本身内,也会以返回值的形式返回,详见代码内文档
|
||||
|
||||
# 使用 form_progress_bar 函数进行转换之后,返回值有三个
|
||||
# 值得注意的是第一个返回值返回的是依照midi频道存储的指令对象列表
|
||||
# 也就是列表套列表
|
||||
cvt_mid.form_progress_bar(
|
||||
max_score, # 音乐时长游戏刻
|
||||
scoreboard_name, # 进度条使用的计分板名称
|
||||
progressbar_style, # 进度条样式组(详见下方)
|
||||
)
|
||||
|
||||
# 同上面生成播放指令的理,进度条指令也会存储至对象内
|
||||
print(
|
||||
"进度条指令如下:\n{}".format(
|
||||
cvt_mid.progress_bar_command
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
在上面的代码中,进度条样式是可以自定义的,详见[下方说明](%E5%BA%93%E7%9A%84%E7%94%9F%E6%88%90%E4%B8%8E%E5%8A%9F%E8%83%BD%E6%96%87%E6%A1%A3.md#进度条自定义)。
|
||||
|
||||
- 转换成指令是一个方面,接下来是再转换为可以导入MC的格式。我们提供了 **音·创** 内置的附加组件,可以借助 `MidiConvert` 对象转换为相应格式。
|
||||
|
||||
```python
|
||||
# 导入 Musicreater
|
||||
import Musicreater
|
||||
# 导入附加组件功能
|
||||
import Musicreater.plugin
|
||||
|
||||
|
||||
# 导入相应的文件格式转换功能
|
||||
|
||||
# 转换为函数附加包
|
||||
import Musicreater.plugin.funpack
|
||||
# 转换为 BDX 结构文件
|
||||
import Musicreater.plugin.bdxfile
|
||||
# 转换为 mcstructure 结构文件
|
||||
import Musicreater.plugin.mcstructfile
|
||||
# 转换为结构附加包
|
||||
import Musicreater.plugin.mcstructpack
|
||||
# 直接通过 websocket 功能播放(正在开发)
|
||||
import Musicreater.plugin.websocket
|
||||
|
||||
|
||||
# 定义转换参数
|
||||
cvt_cfg = Musicreater.plugin.ConvertConfig(
|
||||
output_path,
|
||||
volumn, # 音量大小参数
|
||||
speed, # 速度倍率
|
||||
progressbar, # 进度条样式组(详见下方)
|
||||
)
|
||||
|
||||
# 使用附加组件转换,其调用的函数应为:
|
||||
# Musicreater.plugin.输出格式.播放器格式
|
||||
# 值得注意的是,并非所有输出格式都支持所有播放器格式
|
||||
# 调用的时候还请注意甄别
|
||||
# 例如,以下函数是将 MidiConvert 对象 cvt_mid
|
||||
# 以 cvt_cfg 指定的参数
|
||||
# 以延迟播放器转换为 mcstructure 文件
|
||||
Musicreater.plugin.mcstructfile.to_mcstructure_file_in_delay(
|
||||
cvt_mid,
|
||||
cvt_cfg,
|
||||
)
|
||||
```
|
||||
**此开发文档已经过时**
|
||||
|
||||
> **请明确**:该文档自 音·创 v2 版本停止开发后不再更新,本插件之相关功能从移植到 v3 之后已有变化。因此,此处文档仅作引用和参考,后续更新后会移除这句话。
|
||||
|
||||
# 生成文件结构
|
||||
|
||||
@@ -154,12 +21,12 @@
|
||||
|指令链(链)|与链式指令方块不同,一个指令链通常指代的是一串由某种非链式指令方块作为开头,后面连着一串链式指令方块的结构。|通常的链都应用于需要“单次激活而多指令”的简单功能|
|
||||
|起始块|链最初的那个非链式指令方块。|此方块为脉冲方块或重复方块皆可|
|
||||
|指令系统(系统)|指令系统通常指的是,由一个或多个指令链以及相关红石机构相互配合、一同组成的,为达到某种特定的功能而构建的整体结构。|通常的系统都应用于需要“综合调配指令”的复杂功能。可由多个实现不同功能的模块构成,不同系统之间可以相互调用各自的模块。|
|
||||
|游戏刻(刻)|游戏的一刻是指《我的世界》的游戏进程循环运行一次所占用的时间。([详见《我的世界》中文维基](https://minecraft.fandom.com/zh/wiki/%E5%88%BB#%E6%B8%B8%E6%88%8F%E5%88%BB))。指令方块的延迟功能(即指令方块的“延迟刻数”设置项,此项的名称被误译为“已选中项的延迟”)的单位即为`1`游戏刻。|正常情况下,游戏固定以每秒钟 $20$ 刻的速率运行。但是,由于游戏内的绝大多数操作都是基于游戏进程循环而非现实中的时间来计时并进行的,一次游戏循环内也许会发生大量的操作,更多情况下,一秒对应的游戏刻会更少。|
|
||||
|红石刻|一个红石刻代表了两个游戏刻。([详见《我的世界》中文维基](https://minecraft.fandom.com/zh/wiki/%E5%88%BB#%E7%BA%A2%E7%9F%B3%E5%88%BB))。红石中继器会带来 $1$~$4$ 个红石刻的延迟,其默认的延迟时间为 $1$ 红石刻。|正常情况下,红石信号在一个红石电路中传输回存在 $\frac{1}{10}$ 秒左右的延迟。但是,同理于游戏刻,一秒对应的红石刻是不定的。|
|
||||
|游戏刻(刻)|游戏的一刻是指《我的世界》的游戏进程循环运行一次所占用的时间。([详见《我的世界》中文维基](https://zh.minecraft.wiki/wiki/%E5%88%BB#%E6%B8%B8%E6%88%8F%E5%88%BB))。指令方块的延迟功能(即指令方块的“延迟刻数”设置项,此项的名称被误译为“已选中项的延迟”)的单位即为 `1` 游戏刻。|正常情况下,游戏固定以每秒钟 `20` 刻的速率运行。但是,由于游戏内的绝大多数操作都是基于游戏进程循环而非现实中的时间来计时并进行的,一次游戏循环内也许会发生大量的操作,更多情况下,一秒对应的游戏刻会更少。|
|
||||
|红石刻|一个红石刻代表了两个游戏刻。([详见《我的世界》中文维基](https://zh.minecraft.wiki/wiki/%E5%88%BB#%E7%BA%A2%E7%9F%B3%E5%88%BB))。红石中继器会带来 `1~4` 个红石刻的延迟,其默认的延迟时间为 `1` 红石刻。|正常情况下,红石信号在一个红石电路中传输回存在 $\frac{1}{10}$ 秒左右的延迟。但是,同理于游戏刻,一秒对应的红石刻是不定的。|
|
||||
|
||||
## 播放器
|
||||
|
||||
**音·创**生成的文件可以采用多种方式播放,一类播放方式,我们称其为**播放器**,例如**延迟播放器**和**计分板播放器**等等,以后推出的新的播放器,届时也会在此处更新。
|
||||
本插件生成的文件可以采用多种方式播放,一类播放方式,我们称其为**播放器**,例如**延迟播放器**和**计分板播放器**等等,以后推出的新的播放器,届时也会在此处更新。
|
||||
|
||||
为什么要设计这么多播放器?是为了适应不同的播放环境需要。通常情况下,一个音乐中含有多个音符,音符与音符之间存在间隔,这里就产生了不一样的,实现音符间时间间隔的方式。而不同的应用环境下,又会产生不一样的要求。接下来将对不同的播放器进行详细介绍。
|
||||
|
||||
@@ -170,7 +37,7 @@
|
||||
|`ScBd`|指定的计分板名称||
|
||||
|`Tg`|播放对象|选择器或玩家名|
|
||||
|`x`|音发出时对应的分数值||
|
||||
|`InstID`|声音效果ID|不同的声音ID可以对应不同的乐器,详见转换[乐器对照表](./%E8%BD%AC%E6%8D%A2%E4%B9%90%E5%99%A8%E5%AF%B9%E7%85%A7%E8%A1%A8.md)|
|
||||
|`InstID`|声音效果ID|不同的声音ID可以对应不同的乐器,详见转换[乐器对照表](../../../../Musicreater/blob/master/docs/%E8%BD%AC%E6%8D%A2%E4%B9%90%E5%99%A8%E5%AF%B9%E7%85%A7%E8%A1%A8.md)|
|
||||
|`Ht`|播放点对玩家的距离|通过距离来表达声音的响度。以 $S$ 表示此参数`Ht`,以Vol表示音量百分比,则计算公式为: $S = \frac{1}{Vol}-1$ |
|
||||
|`Vlct`|原生我的世界中规定的播放力度|这个参数是一个谜一样的存在,似乎它的值毫不重要……因为无论这个值是多少,我们听起来都差不多。当此音符所在MIDI通道为第一通道,则这个值为 $0.7$ 倍MIDI指定力度,其他则为 $0.9$ 倍。|
|
||||
|`Ptc`|音符的音高|这是决定音调的参数。以 $P$ 表示此参数, $n$ 表示其在MIDI中的编号, $x$ 表示一定的音调偏移,则计算公式为: $P = 2^\frac{n-60-x}{12}$。之所以存在音调偏移是因为在《我的世界》中,不同的[乐器存在不同的音域](https://zh.minecraft.wiki/wiki/%E9%9F%B3%E7%AC%A6%E7%9B%92#%E4%B9%90%E5%99%A8),我们通过音调偏移来进行调整。|
|
||||
@@ -181,7 +48,7 @@
|
||||
|
||||
计分板播放器是一种传统的《我的世界》音乐播放方式。通过对于计分板加分来实现播放不同的音符。一个很简单的原理,就是**用不同的计分板分值对应不同的音符**,再通过加分,来达到那个分值,即播放出来。
|
||||
|
||||
在**音·创**中,用来达到这种效果的指令是这样的:
|
||||
在**音·创 v2**中,用来达到这种效果的指令是这样的:
|
||||
|
||||
```mcfunction
|
||||
execute @a[scores={ScBd=x}] ~ ~ ~ playsound InstID @s ^ ^ ^Ht Vlct Ptc
|
||||
@@ -195,7 +62,7 @@
|
||||
|
||||
延迟播放器是通过《我的世界》游戏中,指令方块的设置项“延迟刻数”来达到定位音符的效果。**将所有的音符依照其播放时距离乐曲开始时的时间(毫秒),放在一个序列内,再计算音符两两之间对应的时间差值,转换为《我的世界》内对应的游戏刻数之后填入指令方块的设置中。**
|
||||
|
||||
在**音·创**中,由于此方式播放的音乐不需要用计分板,所以播放指令是这样的:
|
||||
在**音·创 v2**中,由于此方式播放的音乐不需要用计分板,所以播放指令是这样的:
|
||||
|
||||
```mcfunction
|
||||
execute Tg ~ ~ ~ playsound InstID @s ^ ^ ^Ht Vlct Ptc
|
||||
@@ -263,7 +130,7 @@
|
||||
我们的结构生成器在生成指令链时,将首先以相对坐标系 $(0, 0, 0)$ (即相对原点)开始,自下向上堆叠高度轴(即 $Y$ 轴)的长,当高度轴达到了限制的高度时,便将 $Z$ 轴向正方向堆叠 $1$ 个方块,并开始自上向下重新堆叠,直至高度轴坐标达到相对为 $0$。若当所生成结构的 $Z$ 轴长达到了其最大延伸长度,则此结构生成器将反转 $Z$ 轴的堆叠方向,直至 $Z$ 轴坐标相对为 $0$。如此往复,直至指令链堆叠完成。
|
||||
|
||||
|
||||
# 进度条自定义
|
||||
# 进度条自定义(已完全过时,请参阅相关代码)
|
||||
|
||||
因为我们提供了可以自动转换进度条的功能,因此在这里给出进度条自定义参数的详细解释。
|
||||
|
||||
@@ -313,3 +180,4 @@ DEFAULT_PROGRESSBAR_STYLE = ProgressBarStyle(
|
||||
|
||||
*为了避免生成错误,请尽量避免使用标识符作为定义样式字符串的其他部分*
|
||||
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
<h1 align="center">音·创 Musicreater</h1>
|
||||
<h2 align="center">Minecraft 指令结构生成插件</h1>
|
||||
|
||||
|
||||
<p align="center">
|
||||
<img width="128" height="128" src="https://gitee.com/TriM-Organization/Musicreater/raw/master/resources/msctIcon.png" >
|
||||
<img width="128" height="128" src="../../../../raw/master/resources/msctIcon.png" >
|
||||
</p>
|
||||
|
||||
# 生成文件的使用
|
||||
<h3 align="center">生成文件的使用方式</h3>
|
||||
|
||||
**此引导文档已经过时**
|
||||
|
||||
> **请明确**:该文档自 音·创 v2 版本停止开发后不再更新,本插件之相关功能从移植到 v3 之后已有变化。因此,此处文档仅作引用和参考,后续更新后会移除这句话。
|
||||
|
||||
*这是本库所生成文件的使用声明,不是使用本库的教程,若要查看**本库的文档**,可点击[此处](./%E5%BA%93%E7%9A%84%E7%94%9F%E6%88%90%E4%B8%8E%E5%8A%9F%E8%83%BD%E6%96%87%E6%A1%A3.md);若要查看有关文件结构的内容,可以点击[此处](./%E7%94%9F%E6%88%90%E6%96%87%E4%BB%B6%E7%BB%93%E6%9E%84%E8%AF%B4%E6%98%8E.md)*
|
||||
|
||||
## 附加包格式
|
||||
|
||||
+237
@@ -0,0 +1,237 @@
|
||||
|
||||
classDiagram
|
||||
|
||||
direction LR
|
||||
|
||||
class Exception{
|
||||
Python 内置基类
|
||||
}
|
||||
|
||||
class MusicreaterBaseException {
|
||||
"[音·创] - ..."
|
||||
所有音·创 v3 错误的基类
|
||||
}
|
||||
|
||||
class MusicreaterInnerlyError {
|
||||
"内部错误 - ..."
|
||||
面向开发者的内部错误
|
||||
}
|
||||
|
||||
class MusicreaterOuterlyError {
|
||||
"外部错误 - ..."
|
||||
面向用户的外部错误
|
||||
}
|
||||
|
||||
class InnerlyParameterError {
|
||||
"内部传参错误 - ..."
|
||||
内部参数相关错误
|
||||
}
|
||||
|
||||
class OuterlyParameterError {
|
||||
"参数错误 - ..."
|
||||
外部参数相关错误
|
||||
}
|
||||
|
||||
class ParameterTypeError {
|
||||
"参数类型错误:..."
|
||||
继承自 InnerlyParameterError 和 TypeError
|
||||
}
|
||||
|
||||
class ParameterValueError {
|
||||
"参数数值错误:..."
|
||||
继承自 InnerlyParameterError 和 ValueError
|
||||
}
|
||||
|
||||
class PluginNotSpecifiedError {
|
||||
"未指定插件:..."
|
||||
继承自 InnerlyParameterError 和 LookupError
|
||||
}
|
||||
|
||||
class ZeroSpeedError {
|
||||
"播放速度为零:..."
|
||||
继承自 OuterlyParameterError 和 ZeroDivisionError
|
||||
}
|
||||
|
||||
class IllegalMinimumVolumeError {
|
||||
"最小播放音量超出范围:..."
|
||||
继承自 OuterlyParameterError 和 ValueError
|
||||
}
|
||||
|
||||
class FileFormatNotSupportedError {
|
||||
"不支持的文件格式:..."
|
||||
继承自 MusicreaterOuterlyError
|
||||
}
|
||||
|
||||
class NoteBinaryDecodeError {
|
||||
"解码音乐存储二进制数据时出现问题 - ..."
|
||||
继承自 MusicreaterOuterlyError
|
||||
}
|
||||
|
||||
class SingleNoteDecodeError {
|
||||
"音符解码出错:..."
|
||||
继承自 NoteBinaryDecodeError
|
||||
}
|
||||
|
||||
class NoteBinaryFileTypeError {
|
||||
"无法识别音乐存储文件对应的类型:..."
|
||||
继承自 NoteBinaryDecodeError
|
||||
}
|
||||
|
||||
class NoteBinaryFileVerificationFailed {
|
||||
"音乐存储文件校验失败:..."
|
||||
继承自 NoteBinaryDecodeError
|
||||
}
|
||||
|
||||
class PluginDefineError {
|
||||
"插件内部错误 - ..."
|
||||
插件定义相关的内部错误
|
||||
}
|
||||
|
||||
class PluginInstanceNotFoundError {
|
||||
"插件实例未找到:..."
|
||||
继承自 PluginDefineError 和 LookupError
|
||||
}
|
||||
|
||||
class PluginAttributeNotFoundError {
|
||||
"插件类的必要属性不存在:..."
|
||||
继承自 PluginDefineError 和 AttributeError
|
||||
}
|
||||
|
||||
class PluginMetainfoError {
|
||||
"插件元信息定义错误 - ..."
|
||||
插件元信息相关错误
|
||||
}
|
||||
|
||||
class PluginMetainfoTypeError {
|
||||
"插件元信息类型错误:..."
|
||||
继承自 PluginMetainfoError 和 TypeError
|
||||
}
|
||||
|
||||
class PluginMetainfoValueError {
|
||||
"插件元信息数值错误:..."
|
||||
继承自 PluginMetainfoError 和 ValueError
|
||||
}
|
||||
|
||||
class PluginMetainfoNotFoundError {
|
||||
"插件元信息未定义:..."
|
||||
继承自 PluginMetainfoError 和 PluginAttributeNotFoundError
|
||||
}
|
||||
|
||||
class PluginLoadError {
|
||||
"插件加载错误 - ..."
|
||||
插件加载相关的外部错误
|
||||
}
|
||||
|
||||
class PluginNotFoundError {
|
||||
"插件未找到:..."
|
||||
继承自 PluginLoadError
|
||||
}
|
||||
|
||||
class PluginRegisteredError {
|
||||
"插件重复注册:..."
|
||||
继承自 PluginLoadError
|
||||
}
|
||||
|
||||
class PluginConfigRelatedError {
|
||||
"插件配置相关错误 - ..."
|
||||
插件配置相关错误基类
|
||||
}
|
||||
|
||||
class PluginConfigLoadError {
|
||||
"插件配置文件加载错误:..."
|
||||
继承自 PluginLoadError、PluginConfigRelatedError
|
||||
}
|
||||
|
||||
class PluginConfigDumpError {
|
||||
"插件配置文件保存错误:..."
|
||||
继承自 PluginConfigRelatedError
|
||||
}
|
||||
%% 高亮定义
|
||||
|
||||
class ParameterTypeError ::: highlight
|
||||
class ParameterValueError ::: highlight
|
||||
class PluginNotSpecifiedError ::: highlight
|
||||
class ZeroSpeedError ::: highlight
|
||||
class IllegalMinimumVolumeError ::: highlight
|
||||
class FileFormatNotSupportedError ::: highlight
|
||||
class SingleNoteDecodeError ::: highlight
|
||||
class NoteBinaryFileTypeError ::: highlight
|
||||
class NoteBinaryFileVerificationFailed ::: highlight
|
||||
class PluginInstanceNotFoundError ::: highlight
|
||||
class PluginAttributeNotFoundError ::: highlight
|
||||
class PluginMetainfoTypeError ::: highlight
|
||||
class PluginMetainfoValueError ::: highlight
|
||||
class PluginMetainfoNotFoundError ::: highlight
|
||||
class PluginNotFoundError ::: highlight
|
||||
class PluginRegisteredError ::: highlight
|
||||
class PluginConfigLoadError ::: highlight
|
||||
class PluginConfigDumpError ::: highlight
|
||||
|
||||
%% 定义高亮样式
|
||||
classDef highlight fill:,stroke-width:5px
|
||||
|
||||
%% 继承关系(箭头从子类指向父类)
|
||||
Exception <|-- MusicreaterBaseException
|
||||
Exception <|-- TypeError
|
||||
Exception <|-- ValueError
|
||||
Exception <|-- LookupError
|
||||
Exception <|-- AttributeError
|
||||
Exception <|-- ZeroDivisionError
|
||||
MusicreaterBaseException <|-- MusicreaterInnerlyError
|
||||
MusicreaterBaseException <|-- MusicreaterOuterlyError
|
||||
|
||||
MusicreaterInnerlyError <|-- InnerlyParameterError
|
||||
MusicreaterOuterlyError <|-- OuterlyParameterError
|
||||
|
||||
InnerlyParameterError <|-- ParameterTypeError
|
||||
TypeError <|-- ParameterTypeError
|
||||
|
||||
InnerlyParameterError <|-- ParameterValueError
|
||||
ValueError <|-- ParameterValueError
|
||||
|
||||
InnerlyParameterError <|-- PluginNotSpecifiedError
|
||||
LookupError <|-- PluginNotSpecifiedError
|
||||
|
||||
OuterlyParameterError <|-- ZeroSpeedError
|
||||
ZeroDivisionError <|-- ZeroSpeedError
|
||||
|
||||
OuterlyParameterError <|-- IllegalMinimumVolumeError
|
||||
ValueError <|-- IllegalMinimumVolumeError
|
||||
|
||||
MusicreaterOuterlyError <|-- FileFormatNotSupportedError
|
||||
MusicreaterOuterlyError <|-- NoteBinaryDecodeError
|
||||
|
||||
NoteBinaryDecodeError <|-- SingleNoteDecodeError
|
||||
NoteBinaryDecodeError <|-- NoteBinaryFileTypeError
|
||||
NoteBinaryDecodeError <|-- NoteBinaryFileVerificationFailed
|
||||
|
||||
MusicreaterInnerlyError <|-- PluginDefineError
|
||||
|
||||
PluginDefineError <|-- PluginInstanceNotFoundError
|
||||
LookupError <|-- PluginInstanceNotFoundError
|
||||
|
||||
PluginDefineError <|-- PluginAttributeNotFoundError
|
||||
AttributeError <|-- PluginAttributeNotFoundError
|
||||
|
||||
PluginDefineError <|-- PluginMetainfoError
|
||||
|
||||
PluginMetainfoError <|-- PluginMetainfoTypeError
|
||||
TypeError <|-- PluginMetainfoTypeError
|
||||
|
||||
PluginMetainfoError <|-- PluginMetainfoValueError
|
||||
ValueError <|-- PluginMetainfoValueError
|
||||
|
||||
PluginMetainfoError <|-- PluginMetainfoNotFoundError
|
||||
PluginAttributeNotFoundError <|-- PluginMetainfoNotFoundError
|
||||
|
||||
MusicreaterOuterlyError <|-- PluginLoadError
|
||||
|
||||
PluginLoadError <|-- PluginNotFoundError
|
||||
PluginLoadError <|-- PluginRegisteredError
|
||||
|
||||
MusicreaterOuterlyError <|-- PluginConfigRelatedError
|
||||
|
||||
PluginLoadError <|-- PluginConfigLoadError
|
||||
PluginConfigRelatedError <|-- PluginConfigLoadError
|
||||
|
||||
PluginConfigRelatedError <|-- PluginConfigDumpError
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 652 KiB |
+185
@@ -0,0 +1,185 @@
|
||||
|
||||
# 教程:编写插件
|
||||
|
||||
> 版权所有 © 2026 金羿
|
||||
> Copyright © 2026 Eilles
|
||||
|
||||
睿乐组织 开发交流群 [861684859](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=fxNYIX_zKMgaO8X6K7pP7tHtLB7JRvdX&noverify=0&group_code=861684859)
|
||||
Email [TriM-Organization@hotmail.com](mailto:TriM-Organization@hotmail.com)
|
||||
|
||||
```license
|
||||
本示例模块开放授权,同时,本教程文件已开放至公共领域。
|
||||
请注意:
|
||||
若是对本文件的直接转载(在形式上没有修改、增删、添加注释,或单纯修改排版、翻译、录屏、截图)
|
||||
则该使用者需要在转载所及之处,明确在转载的内容开头标注本文之原始著作权人
|
||||
在当前文件下,该原始著作权人为金羿(Eilles)
|
||||
如果是对本文进行了一定程度上的修改和补充、或者以不同方式演绎本文件(如制成视频教程等)
|
||||
则无需标注原作者,允许该使用者自行署名
|
||||
|
||||
本声明仅限于包含此声明的本文件,本声明与项目内其他文件无关。
|
||||
本声明同样适用于所有直接转载的内容。
|
||||
```
|
||||
|
||||
本教程文档的关联文件是:
|
||||
- 全曲导入、音轨导入插件示例:[exp_importdata_plugin.py](../examples/exp_importdata_plugin.py)
|
||||
- 导出曲目、导出音轨插件示例:[exp_dataexport_plugin.py](../examples/exp_dataexport_plugin.py)
|
||||
|
||||
## 新建文件
|
||||
|
||||
### 基础模块知识
|
||||
|
||||
首先,一个 **音·创 v3** 的插件应当存储于一个 Python 模块之中,也就是插件存在于可以被 import 语句引入的 module 中。
|
||||
|
||||
这就意味着,承载插件的模块本质上可以是多个 Python 的 `.py` 文件组成的,带有 `__init__.py` 的一个文件夹;
|
||||
或者是一个简单的 `.py` 文件。
|
||||
|
||||
我们有这种共识:大家已经知道了模块的相关知识,后面的教程中你将会理解 **音·创 v3** 插件是什么东西,以及它和 Python 模块的关联和区别。
|
||||
|
||||
## 开始动笔
|
||||
|
||||
### 插件配置
|
||||
|
||||
如果插件需要配置项,则需进行此节。
|
||||
|
||||
从 `Musicreater.plugins` 导入 `PluginConfig` 类,并从此继承一个类,且须用 dataclass 装饰器来注册之:这就成为了一个插件的配置类。
|
||||
_对于这个 `dataclass` “数据类”的使用方式,可以阅读 dataclass 的官方文档,或者直接在实例后面打个 `.`,让代码提示告诉你它能干什么_
|
||||
|
||||
```python
|
||||
from Musicreater.plugins import PluginConfig
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass
|
||||
class ExampleImportConfig(PluginConfig):
|
||||
example_config_item3: bool
|
||||
example_config_item1: str = "example_config_item"
|
||||
example_config_item2: int = 0
|
||||
```
|
||||
|
||||
## 编写插件
|
||||
|
||||
### 导入所需项目
|
||||
|
||||
首先在代码开头导入插件所需的东西。
|
||||
|
||||
在此之前,我们明确:一个 **音·创 v3** 的插件应当是一个继承自我们已经准备好的插件基类的**类**(缩句:插件是类);
|
||||
在 **音·创 v3** 中,任何对音乐的操作,包括**导入**、**处理**、**导出**,都分为对 **整首曲目** 的操作和对 **单个音轨** 的操作。
|
||||
|
||||
在这里我们首先要对插件的类型进行判别,根据以下表格,可以得出所用功能对应的插件类型:
|
||||
|
||||
| 功能\对象 | 完整曲目 | 单个音轨 |
|
||||
|----------|----------|----------|
|
||||
| 导入数据 | `PluginTypes.FUNCTION_MUSIC_IMPORT` | `PluginTypes.FUNCTION_TRACK_IMPORT` |
|
||||
| 数据处理 | `PluginTypes.FUNCTION_MUSIC_OPERATE` | `PluginTypes.FUNCTION_TRACK_OPERATE` |
|
||||
| 导出数据 | `PluginTypes.FUNCTION_MUSIC_EXPORT` | `PluginTypes.FUNCTION_TRACK_EXPORT` |
|
||||
| 支持库 | `PluginTypes.LIBRARY` | 同左 |
|
||||
| 提供服务 | `PluginTypes.SERVICE` | 同左 |
|
||||
|
||||
也就是说,除了 `PluginTypes.LIBRARY` 和 `PluginTypes.SERVICE` 是不按照处理对象做区分的外,其余的这些都是对数据进行处理的插件、因此是做了处理数据的类型区分的。
|
||||
|
||||
我们对每个不同类型的插件都做了专用的抽象基类和一个装饰器函数。因为插件本身就是类,所以对应类型的插件只需要继承我们提供的抽象基类,并通过装饰器函数注册即可。(具体写法在后面会说哦)
|
||||
|
||||
也就是说,如果我们要写的是一个用来导入音乐的、对整个曲目进行处理的插件,那么就需要导入 `MusicInputPluginBase` 类和 `music_input_plugin` 函数以便后续调用。
|
||||
|
||||
同时,既然要导入内容,那就一并把 `PluginMetaInformation` 类和 `PluginTypes` 类也导入了吧,这是定义插件的信息所需要的。也就是说,这样的话,我们在导入部分就应该这样写:
|
||||
|
||||
```python
|
||||
from Musicreater.plugins import (
|
||||
music_input_plugin,
|
||||
PluginMetaInformation,
|
||||
PluginTypes,
|
||||
MusicInputPluginBase,
|
||||
)
|
||||
```
|
||||
|
||||
### 定义信息
|
||||
|
||||
接着我们来定义一个插件的信息并将其注册。
|
||||
|
||||
假设我们想要做一个对**整首曲目**进行**导入操作**的插件(参照前面举的例子),那么就需要继承 `MusicInputPluginBase` 类。
|
||||
|
||||
> 请注意:插件类的类名称不得以 `Base` 结尾,因为咱写的是插件,不是插件基类。
|
||||
|
||||
在插件的类的开头,需要用插件注册装饰函数来对插件类装饰。
|
||||
|
||||
```python
|
||||
@music_input_plugin("example_import_plugin")
|
||||
class xxx:
|
||||
...
|
||||
```
|
||||
|
||||
我们这里对应插件类型的注册器是 `music_input_plugin` 函数。
|
||||
在注册器函数后的参数,是这个插件的惟一识别码。不应与其他任何插件混淆。
|
||||
通常,这个惟一识别码可以是这个插件的功能描述或者就是插件名。
|
||||
|
||||
接着编写这个插件,也即是此类。
|
||||
|
||||
每个插件的类必须包含一个用于指定插件元信息的 `metainfo` 属性。
|
||||
如果插件是导入数据或者导出数据的插件,则必须包含一个 `supported_formats` 属性,用以声明插件所支持的数据格式。
|
||||
|
||||
对于插件的元信息,我们规定为一个 `PluginMetaInformation` 实例,这个实例需要的参数如下:
|
||||
|
||||
```python
|
||||
# 注册插件
|
||||
@music_input_plugin("something_convert_to_music")
|
||||
# 继承自对应类型的插件基类
|
||||
class ExampleImportPlugin(MusicInputPluginBase):
|
||||
|
||||
# 插件元信息定义
|
||||
metainfo = PluginMetaInformation(
|
||||
name="示例导入插件", # 插件名称
|
||||
author="金羿", # 插件作者
|
||||
description="这是一个示例导入插件", # 插件描述
|
||||
version=(0, 0, 1), # 插件版本
|
||||
type=PluginTypes.FUNCTION_MUSIC_IMPORT, # 插件类型,需要和注册的类型与继承的基类相符合
|
||||
license="The Unlicense", # 插件许可证(可缺省,默认为字符串 `MIT License`)
|
||||
dependencies=("something_convertion_library") # 插件对于其他插件的依赖项(可缺省,默认为空元组)
|
||||
)
|
||||
```
|
||||
|
||||
对于实现导入导出数据的功能的插件,`supported_formats` 属性应当是一个元组,其中最好以全字母大写的字符串形式列出支持的**文件格式**或者**数据格式**(如果定义的时候没有大写的话,内部会自动处理成大写的,所以插件类的实例后面也会变成大写,这个时候,因为原定义是小写,有可能造成混淆,所以尽量不要写小写)。例如一个处理 `.mp4` 文件格式的插件可以这样写:
|
||||
|
||||
```python
|
||||
@...
|
||||
class ...:
|
||||
...
|
||||
|
||||
supported_formats = ("MP4", "MPEG4", "MPEG-4")
|
||||
```
|
||||
|
||||
至此,你已经完成了插件基本信息的定义。
|
||||
|
||||
### 实现功能
|
||||
|
||||
根据插件的类型不同,每个插件都需要实现至少一个指定的方法。如下表所示:
|
||||
|
||||
| 插件功能 | 必须实现的方法 | 类型描述 | 可选实现的方法| 可选方法类型描述 | 备注 |
|
||||
| ------ | ------------ | - | ----------- | - |----|
|
||||
| 导入数据 | `loadbytes` | `Callable[[BinaryIO, Optional[PluginConfig]], T@插件处理对象类型]` | `load` | `Callable[[Path, Optional[PluginConfig]], T@插件处理对象类型]` | 如果 `load` 方法不单独实现,则会自动在打开文件后将文件 IO 变量传入 `loadbytes` 中并返回之 |
|
||||
| 数据处理 | `process` | `Callable[[T@插件处理对象类型, Optional[PluginConfig]], T@插件处理对象类型]` | 无 | 无 | 根据处理对象是完整曲目(`SingleMusic`)还是单个音轨(`SingleTrack`),返回也是一样的。导入导出数据相关的插件亦皆同此说。 |
|
||||
| 导出数据 | `stream_dump` | `Callable[[T@插件处理对象类型, Optional[PluginConfig]], Iterator[bytes]]` | `dump` | `Callable[[T@插件处理对象类型, Path, Optional[PluginConfig]], None]` | 若未重写 `dump` 方法,基类已提供默认实现:逐块写入 `stream_dump` 的结果 |
|
||||
| 支持库 | 无 | 无 | 无 | 无 | 无 |
|
||||
| 服务 | `serve` | `Callable[[Optional[PluginConfig]], None]` | 无 | 无 | 用于提供后台服务或一次性任务,由运行时调用(暂无设计思路,相关讨论请见[项目待办清单](../TO-DO.md#讨论)) |
|
||||
|
||||
也就是说,举个例子:一个**用于导入**的插件类必须定义一个 `loadbytes` 方法,用于从字节流中导入数据。可选是否单独实现 `load` 方法,如果不单独实现,则已经继承的方法会在调用时,直接通过打开文件后传参数给 `loadbytes` 来实现。
|
||||
|
||||
```python
|
||||
@...
|
||||
class ExampleImportPlugin(MusicInputPluginBase):
|
||||
...
|
||||
# 定义 loadbytes 方法,从字节流中导入数据
|
||||
def loadbytes(
|
||||
self, bytes_buffer_in: BinaryIO, config: Optional[ExampleImportConfig]
|
||||
) -> "SingleMusic":
|
||||
... # 这里写功能实现
|
||||
|
||||
# 插件可选地定义 load 方法,从文件导入数据。下面展示的是不定义 load 方法时候的实现方式
|
||||
def load(
|
||||
self, file_path: Path, config: Optional[ExampleImportConfig]
|
||||
) -> "SingleMusic":
|
||||
with file_path.open("rb") as f:
|
||||
return self.loadbytes(f, config)
|
||||
```
|
||||
|
||||
至此,一个插件的编写已经完成。
|
||||
|
||||
同时,如果有不清楚的地方,可以查看我们的[内置插件](../Musicreater/builtin_plugins/),说不定会给你一些启发。
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
# 音乐序列文件格式
|
||||
# 音乐序列文件格式(已过时)
|
||||
|
||||
音·创 库的音符序列文件格式包含两种,一种是常规的音乐序列存储采用的 MSQ 格式,另一种是为了流式读取音符而采用的 FSQ 格式。
|
||||
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
示例插件:导出成其他文件
|
||||
"""
|
||||
|
||||
"""
|
||||
版权所有 © 2026 金羿
|
||||
Copyright © 2026 Eilles
|
||||
"""
|
||||
|
||||
# 睿乐组织 开发交流群 861684859
|
||||
# Email TriM-Organization@hotmail.com
|
||||
|
||||
"""
|
||||
本示例模块开放授权,本文件已开放至公共领域。
|
||||
请注意:
|
||||
若是对本文件的直接转载(在形式上没有修改、增删、添加注释,或单纯修改排版、翻译、录屏、截图)
|
||||
则该使用者需要在转载所及之处,明确在转载的内容开头标注本文之原始著作权人
|
||||
在当前文件下,该原始著作权人为金羿(Eilles)
|
||||
如果是对本文进行了一定程度上的修改和补充、或者以不同方式演绎本文件(如制成视频教程等)
|
||||
则无需标注原作者,允许该使用者自行署名
|
||||
|
||||
本声明仅限于包含此声明的本文件,本声明与项目内其他文件无关。
|
||||
本声明同样适用于所有直接转载的内容。
|
||||
"""
|
||||
|
||||
from typing import BinaryIO, Optional, Iterator, Generator, Any, Tuple
|
||||
from pathlib import Path
|
||||
from dataclasses import dataclass
|
||||
|
||||
from Musicreater import SingleMusic, SingleTrack
|
||||
from Musicreater.plugins import (
|
||||
PluginConfig,
|
||||
PluginMetaInformation,
|
||||
PluginTypes,
|
||||
music_output_plugin,
|
||||
MusicOutputPluginBase,
|
||||
track_output_plugin,
|
||||
TrackOutputPluginBase,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExampleExportConfig(PluginConfig):
|
||||
example_config_item3: bool
|
||||
example_config_item1: str = "example_config_item"
|
||||
example_config_item2: int = 0
|
||||
|
||||
def __iter__(self) -> Generator[Tuple[str, Any], None, None]:
|
||||
for k, v in self.to_dict().items():
|
||||
yield k, v
|
||||
|
||||
|
||||
@music_output_plugin("convert_music_to_something")
|
||||
class ExampleExportMusicPlugin(MusicOutputPluginBase):
|
||||
metainfo = PluginMetaInformation(
|
||||
name="示例导出插件·甲",
|
||||
author="金羿",
|
||||
description="这是一个示例导出插件,演示整首曲目导出到其他文件格式的插件的编写过程",
|
||||
version=(0, 0, 1),
|
||||
type=PluginTypes.FUNCTION_MUSIC_EXPORT,
|
||||
license="The Unlicense",
|
||||
dependencies=("something_convertion_library"),
|
||||
)
|
||||
|
||||
supported_formats = ("EXP", "EXAMPLE_FORMAT")
|
||||
|
||||
@staticmethod
|
||||
def something_data_convert(*args) -> bytes:
|
||||
return b"This is something wonderful"
|
||||
|
||||
def stream_dump(
|
||||
self, data: SingleMusic, config: ExampleExportConfig | None
|
||||
) -> Iterator[bytes]:
|
||||
if not config:
|
||||
config = ExampleExportConfig(True)
|
||||
for cfg in config:
|
||||
yield self.something_data_convert(cfg)
|
||||
|
||||
# 插件可选地定义 dump 方法,导出音乐数据到文件。下面展示的是,在不定义 dump 方法时候的,默认的实现方式
|
||||
def dump(
|
||||
self, data: SingleMusic, file_path: Path, config: ExampleExportConfig | None
|
||||
):
|
||||
|
||||
with file_path.open("wb") as f:
|
||||
for _bytes in self.stream_dump(data, config):
|
||||
f.write(_bytes)
|
||||
|
||||
|
||||
@track_output_plugin("convert_track_to_something")
|
||||
class ExampleImportTrackPlugin(TrackOutputPluginBase):
|
||||
metainfo = PluginMetaInformation(
|
||||
name="示例导出插件·乙",
|
||||
author="金羿",
|
||||
description="这是一个示例导出插件,演示从音轨导出的其他格式的插件的编写过程",
|
||||
version=(0, 0, 1),
|
||||
type=PluginTypes.FUNCTION_TRACK_EXPORT,
|
||||
license="The Unlicense",
|
||||
# 可以缺省依赖,如果不需要的话
|
||||
)
|
||||
|
||||
supported_formats = ("EXP", "example_format")
|
||||
|
||||
def stream_dump(
|
||||
self, data: SingleTrack, config: ExampleExportConfig | None
|
||||
) -> Iterator[bytes]:
|
||||
if not config:
|
||||
config = ExampleExportConfig(True)
|
||||
for cfg in config:
|
||||
yield ExampleExportMusicPlugin.something_data_convert(cfg)
|
||||
|
||||
# 可以缺省 dump 方法,会直接用上面展示过的方法输出
|
||||
@@ -0,0 +1,97 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
示例插件:导入音符数据
|
||||
"""
|
||||
|
||||
"""
|
||||
版权所有 © 2026 金羿
|
||||
Copyright © 2026 Eilles
|
||||
"""
|
||||
|
||||
# 睿乐组织 开发交流群 861684859
|
||||
# Email TriM-Organization@hotmail.com
|
||||
|
||||
"""
|
||||
本示例模块开放授权,本文件已开放至公共领域。
|
||||
请注意:
|
||||
若是对本文件的直接转载(在形式上没有修改、增删、添加注释,或单纯修改排版、翻译、录屏、截图)
|
||||
则该使用者需要在转载所及之处,明确在转载的内容开头标注本文之原始著作权人
|
||||
在当前文件下,该原始著作权人为金羿(Eilles)
|
||||
如果是对本文进行了一定程度上的修改和补充、或者以不同方式演绎本文件(如制成视频教程等)
|
||||
则无需标注原作者,允许该使用者自行署名
|
||||
|
||||
本声明仅限于包含此声明的本文件,本声明与项目内其他文件无关。
|
||||
本声明同样适用于所有直接转载的内容。
|
||||
"""
|
||||
|
||||
from typing import BinaryIO, Optional
|
||||
from pathlib import Path
|
||||
from dataclasses import dataclass
|
||||
|
||||
from Musicreater import SingleMusic, SingleTrack
|
||||
from Musicreater.plugins import (
|
||||
PluginConfig,
|
||||
PluginMetaInformation,
|
||||
PluginTypes,
|
||||
music_input_plugin,
|
||||
MusicInputPluginBase,
|
||||
track_input_plugin,
|
||||
TrackInputPluginBase,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExampleImportConfig(PluginConfig):
|
||||
example_config_item3: bool
|
||||
example_config_item1: str = "example_config_item"
|
||||
example_config_item2: int = 0
|
||||
|
||||
|
||||
@music_input_plugin("something_convert_to_music")
|
||||
class ExampleImportMusicPlugin(MusicInputPluginBase):
|
||||
metainfo = PluginMetaInformation(
|
||||
name="示例导入插件·甲",
|
||||
author="金羿",
|
||||
description="这是一个示例导入插件,演示导入到全曲的插件编写过程",
|
||||
version=(0, 0, 1),
|
||||
type=PluginTypes.FUNCTION_MUSIC_IMPORT,
|
||||
license="The Unlicense",
|
||||
dependencies=("something_convertion_library"),
|
||||
)
|
||||
|
||||
supported_formats = ("EXP", "EXAMPLE_FORMAT")
|
||||
|
||||
def loadbytes(
|
||||
self, bytes_buffer_in: BinaryIO, config: Optional[ExampleImportConfig]
|
||||
) -> "SingleMusic":
|
||||
return SingleMusic()
|
||||
|
||||
# 插件可选地定义 load 方法,从文件导入数据。下面展示的是不定义 load 方法时候的实现方式
|
||||
def load(
|
||||
self, file_path: Path, config: Optional[ExampleImportConfig]
|
||||
) -> "SingleMusic":
|
||||
with file_path.open("rb") as f:
|
||||
return self.loadbytes(f, config)
|
||||
|
||||
|
||||
@track_input_plugin("something_convert_to_track")
|
||||
class ExampleImportTrackPlugin(TrackInputPluginBase):
|
||||
metainfo = PluginMetaInformation(
|
||||
name="示例导入插件·乙",
|
||||
author="金羿",
|
||||
description="这是一个示例导入插件,演示导入到音轨的插件编写过程",
|
||||
version=(0, 0, 1),
|
||||
type=PluginTypes.FUNCTION_TRACK_IMPORT,
|
||||
license="The Unlicense",
|
||||
# 可以缺省依赖,如果不需要的话
|
||||
)
|
||||
|
||||
supported_formats = ("EXP", "example_format")
|
||||
|
||||
def loadbytes(
|
||||
self, bytes_buffer_in: BinaryIO, config: Optional[ExampleImportConfig]
|
||||
) -> "SingleTrack":
|
||||
return SingleTrack()
|
||||
|
||||
# 可以缺省 load 方法,会直接用上面展示过的方法读取数据
|
||||
@@ -22,16 +22,16 @@ from .old_exceptions import *
|
||||
from .old_main import (
|
||||
MM_CLASSIC_PERCUSSION_INSTRUMENT_TABLE,
|
||||
MM_CLASSIC_PITCHED_INSTRUMENT_TABLE,
|
||||
MM_TOUCH_PERCUSSION_INSTRUMENT_TABLE,
|
||||
MM_TOUCH_PITCHED_INSTRUMENT_TABLE,
|
||||
MM_DEFAULT_PERCUSSION_INSTRUMENT_TABLE,
|
||||
MM_DEFAULT_PITCHED_INSTRUMENT_TABLE,
|
||||
MidiConvert,
|
||||
mido,
|
||||
)
|
||||
|
||||
from .constants import MIDI_PAN, MIDI_PROGRAM, MIDI_VOLUME
|
||||
from .old_main import MIDI_PAN, MIDI_PROGRAM, MIDI_VOLUME
|
||||
from .subclass import *
|
||||
from .old_types import ChannelType, FittingFunctionType
|
||||
from .utils import *
|
||||
from .old_utils import *
|
||||
|
||||
|
||||
class FutureMidiConvertLyricSupport(MidiConvert):
|
||||
@@ -106,7 +106,7 @@ class FutureMidiConvertLyricSupport(MidiConvert):
|
||||
"{}:{:.2f}".format(mc_sound_ID, mc_pitch),
|
||||
)
|
||||
),
|
||||
tick_delay=tickdelay,
|
||||
delay=tickdelay,
|
||||
),
|
||||
)
|
||||
if using_lyric and note.extra_info["LYRIC_TEXT"]:
|
||||
@@ -140,8 +140,8 @@ class FutureMidiConvertKamiRES(MidiConvert):
|
||||
default_program_value: int = -1,
|
||||
default_volume_value: int = 64,
|
||||
default_tempo_value: int = mido.midifiles.midifiles.DEFAULT_TEMPO,
|
||||
pitched_note_rtable: MidiInstrumentTableType = MM_TOUCH_PITCHED_INSTRUMENT_TABLE,
|
||||
percussion_note_rtable: MidiInstrumentTableType = MM_TOUCH_PERCUSSION_INSTRUMENT_TABLE,
|
||||
pitched_note_rtable: MidiInstrumentTableType = MM_DEFAULT_PITCHED_INSTRUMENT_TABLE,
|
||||
percussion_note_rtable: MidiInstrumentTableType = MM_DEFAULT_PERCUSSION_INSTRUMENT_TABLE,
|
||||
vol_processing_function: FittingFunctionType = velocity_2_distance_natural,
|
||||
pan_processing_function: FittingFunctionType = panning_2_rotation_trigonometric,
|
||||
note_rtable_replacement: Dict[str, str] = {},
|
||||
@@ -182,10 +182,10 @@ class FutureMidiConvertKamiRES(MidiConvert):
|
||||
raise ZeroSpeedError("播放速度为 0 ,其需要(0,1]范围内的实数。")
|
||||
|
||||
# 一个midi中仅有16个通道 我们通过通道来识别而不是音轨
|
||||
midi_channels: MineNoteChannelType = empty_midi_channels(default_staff=[])
|
||||
midi_channels: MineNoteChannelType = enumerated_stuff_copy(staff=[])
|
||||
|
||||
channel_controler: Dict[int, Dict[str, int]] = empty_midi_channels(
|
||||
default_staff={
|
||||
channel_controler: Dict[int, Dict[str, int]] = enumerated_stuff_copy(
|
||||
staff={
|
||||
MIDI_PROGRAM: default_program_value,
|
||||
MIDI_VOLUME: default_volume_value,
|
||||
MIDI_PAN: 64,
|
||||
@@ -205,7 +205,7 @@ class FutureMidiConvertKamiRES(MidiConvert):
|
||||
int,
|
||||
]
|
||||
],
|
||||
] = empty_midi_channels(default_staff=[])
|
||||
] = enumerated_stuff_copy(staff=[])
|
||||
note_queue_B: Dict[
|
||||
int,
|
||||
List[
|
||||
@@ -214,7 +214,7 @@ class FutureMidiConvertKamiRES(MidiConvert):
|
||||
int,
|
||||
]
|
||||
],
|
||||
] = empty_midi_channels(default_staff=[])
|
||||
] = enumerated_stuff_copy(staff=[])
|
||||
|
||||
# 直接使用mido.midifiles.tracks.merge_tracks转为单轨
|
||||
# 采用的时遍历信息思路
|
||||
@@ -469,7 +469,7 @@ class FutureMidiConvertKamiRES(MidiConvert):
|
||||
mc_sound_ID,
|
||||
)
|
||||
),
|
||||
tick_delay=tickdelay,
|
||||
delay=tickdelay,
|
||||
),
|
||||
)
|
||||
delaytime_previous = note.start_tick
|
||||
@@ -501,7 +501,7 @@ class FutureMidiConvertJavaE(MidiConvert):
|
||||
-------
|
||||
list[MineCommand,]
|
||||
"""
|
||||
pgs_style = progressbar_style.base_style
|
||||
pgs_style = progressbar_style.style_base_string
|
||||
"""用于被替换的进度条原始样式"""
|
||||
|
||||
"""
|
||||
@@ -686,8 +686,8 @@ class FutureMidiConvertJavaE(MidiConvert):
|
||||
|
||||
for i in range(pgs_style.count("_")):
|
||||
npg_stl = (
|
||||
pgs_style.replace("_", progressbar_style.played_style, i + 1)
|
||||
.replace("_", progressbar_style.to_play_style)
|
||||
pgs_style.replace("_", progressbar_style.progress_played, i + 1)
|
||||
.replace("_", progressbar_style.progress_toplay)
|
||||
.replace(r"%%N", self.music_name)
|
||||
.replace(
|
||||
r"%%s",
|
||||
@@ -1006,7 +1006,7 @@ class FutureMidiConvertM4(MidiConvert):
|
||||
"{}:{:.2f}".format(mc_sound_ID, mc_pitch),
|
||||
)
|
||||
),
|
||||
tick_delay=tickdelay,
|
||||
delay=tickdelay,
|
||||
),
|
||||
)
|
||||
delaytime_previous = note.start_tick
|
||||
@@ -1042,7 +1042,7 @@ class FutureMidiConvertM5(MidiConvert):
|
||||
# )
|
||||
|
||||
# 一个midi中仅有16个通道 我们通过通道来识别而不是音轨
|
||||
midi_channels: ChannelType = empty_midi_channels()
|
||||
midi_channels: ChannelType = enumerated_stuff_copy()
|
||||
tempo = 500000
|
||||
|
||||
# 我们来用通道统计音乐信息
|
||||
@@ -1052,7 +1052,7 @@ class FutureMidiConvertM5(MidiConvert):
|
||||
if not track:
|
||||
continue
|
||||
|
||||
note_queue = empty_midi_channels(default_staff=[])
|
||||
note_queue = enumerated_stuff_copy(staff=[])
|
||||
|
||||
for msg in track:
|
||||
if msg.time != 0:
|
||||
@@ -1197,7 +1197,7 @@ class FutureMidiConvertM5(MidiConvert):
|
||||
results.append(
|
||||
MineCommand(
|
||||
tracks[all_ticks[i]][j],
|
||||
tick_delay=(
|
||||
delay=(
|
||||
(
|
||||
0
|
||||
if (
|
||||
|
||||
@@ -34,14 +34,6 @@ class MSCTBaseException(Exception):
|
||||
raise self
|
||||
|
||||
|
||||
class MidiFormatException(MSCTBaseException):
|
||||
"""音·创 的所有MIDI格式错误均继承于此"""
|
||||
|
||||
def __init__(self, *args):
|
||||
"""音·创 的所有MIDI格式错误均继承于此"""
|
||||
super().__init__("MIDI 格式错误", *args)
|
||||
|
||||
|
||||
class MidiDestroyedError(MSCTBaseException):
|
||||
"""Midi文件损坏"""
|
||||
|
||||
@@ -82,84 +74,3 @@ class CommandFormatError(MSCTBaseException, RuntimeError):
|
||||
# 那么这两个音符的音长无法判断。这是个好问题,但是不是我现在能解决的,也不是我们现在想解决的问题
|
||||
|
||||
|
||||
class NotDefineTempoError(MidiFormatException):
|
||||
"""没有Tempo设定导致时间无法计算的错误"""
|
||||
|
||||
def __init__(self, *args):
|
||||
"""没有Tempo设定导致时间无法计算的错误"""
|
||||
super().__init__("在曲目开始时没有声明 Tempo(未指定拍长)", *args)
|
||||
|
||||
|
||||
class ChannelOverFlowError(MidiFormatException):
|
||||
"""一个midi中含有过多的通道"""
|
||||
|
||||
def __init__(self, max_channel=16, *args):
|
||||
"""一个midi中含有过多的通道"""
|
||||
super().__init__("含有过多的通道(数量应≤{})".format(max_channel), *args)
|
||||
|
||||
|
||||
class NotDefineProgramError(MidiFormatException):
|
||||
"""没有Program设定导致没有乐器可以选择的错误"""
|
||||
|
||||
def __init__(self, *args):
|
||||
"""没有Program设定导致没有乐器可以选择的错误"""
|
||||
super().__init__("未指定演奏乐器", *args)
|
||||
|
||||
|
||||
class NoteOnOffMismatchError(MidiFormatException):
|
||||
"""音符开音和停止不匹配的错误"""
|
||||
|
||||
def __init__(self, *args):
|
||||
"""音符开音和停止不匹配的错误"""
|
||||
super().__init__("音符不匹配", *args)
|
||||
|
||||
|
||||
class LyricMismatchError(MSCTBaseException):
|
||||
"""歌词匹配解析错误"""
|
||||
|
||||
def __init__(self, *args):
|
||||
"""有可能产生了错误的歌词解析"""
|
||||
super().__init__("歌词解析错误", *args)
|
||||
|
||||
# 已重构
|
||||
class ZeroSpeedError(MSCTBaseException, ZeroDivisionError):
|
||||
"""以0作为播放速度的错误"""
|
||||
|
||||
def __init__(self, *args):
|
||||
"""以0作为播放速度的错误"""
|
||||
super().__init__("播放速度为零", *args)
|
||||
|
||||
# 已重构
|
||||
class IllegalMinimumVolumeError(MSCTBaseException, ValueError):
|
||||
"""最小播放音量有误的错误"""
|
||||
|
||||
def __init__(self, *args):
|
||||
"""最小播放音量错误"""
|
||||
super().__init__("最小播放音量超出范围", *args)
|
||||
|
||||
|
||||
# 已重构
|
||||
class MusicSequenceDecodeError(MSCTBaseException):
|
||||
"""音乐序列解码错误"""
|
||||
|
||||
def __init__(self, *args):
|
||||
"""音乐序列无法正确解码的错误"""
|
||||
super().__init__("解码音符序列文件时出现问题", *args)
|
||||
|
||||
|
||||
# 已重构
|
||||
class MusicSequenceTypeError(MSCTBaseException):
|
||||
"""音乐序列类型错误"""
|
||||
|
||||
def __init__(self, *args):
|
||||
"""无法识别音符序列字节码的类型"""
|
||||
super().__init__("错误的音符序列字节类型", *args)
|
||||
|
||||
|
||||
# 已重构
|
||||
class MusicSequenceVerificationFailed(MusicSequenceDecodeError):
|
||||
"""音乐序列校验失败"""
|
||||
|
||||
def __init__(self, *args):
|
||||
"""音符序列文件与其校验值不一致"""
|
||||
super().__init__("音符序列文件校验失败", *args)
|
||||
|
||||
@@ -65,8 +65,8 @@ __all__ = [
|
||||
"MM_INSTRUMENT_DEVIATION_TABLE",
|
||||
"MM_CLASSIC_PITCHED_INSTRUMENT_TABLE",
|
||||
"MM_CLASSIC_PERCUSSION_INSTRUMENT_TABLE",
|
||||
"MM_TOUCH_PITCHED_INSTRUMENT_TABLE",
|
||||
"MM_TOUCH_PERCUSSION_INSTRUMENT_TABLE",
|
||||
"MM_DEFAULT_PITCHED_INSTRUMENT_TABLE",
|
||||
"MM_DEFAULT_PERCUSSION_INSTRUMENT_TABLE",
|
||||
"MM_DISLINK_PITCHED_INSTRUMENT_TABLE",
|
||||
"MM_DISLINK_PERCUSSION_INSTRUMENT_TABLE",
|
||||
"MM_NBS_PITCHED_INSTRUMENT_TABLE",
|
||||
@@ -85,7 +85,14 @@ __all__ = [
|
||||
"midi_inst_to_mc_sound",
|
||||
]
|
||||
|
||||
from .old_main import MusicSequence, MidiConvert
|
||||
from .old_main import (
|
||||
MusicSequence,
|
||||
MidiConvert,
|
||||
# 字典键
|
||||
MIDI_PROGRAM,
|
||||
MIDI_PAN,
|
||||
MIDI_VOLUME,
|
||||
)
|
||||
|
||||
from .subclass import (
|
||||
MineNote,
|
||||
@@ -96,7 +103,7 @@ from .subclass import (
|
||||
DEFAULT_PROGRESSBAR_STYLE,
|
||||
)
|
||||
|
||||
from .utils import (
|
||||
from .old_utils import (
|
||||
# 兼容性函数
|
||||
load_decode_musicsequence_metainfo,
|
||||
load_decode_msq_flush_release,
|
||||
@@ -104,21 +111,26 @@ from .utils import (
|
||||
# 工具函数
|
||||
guess_deviation,
|
||||
midi_inst_to_mc_sound,
|
||||
# 处理用函数
|
||||
velocity_2_distance_natural,
|
||||
velocity_2_distance_straight,
|
||||
panning_2_rotation_linear,
|
||||
panning_2_rotation_trigonometric,
|
||||
)
|
||||
|
||||
from .constants import (
|
||||
# 字典键
|
||||
MIDI_PROGRAM,
|
||||
MIDI_PAN,
|
||||
MIDI_VOLUME,
|
||||
# 默认值
|
||||
MIDI_DEFAULT_PROGRAM_VALUE,
|
||||
from Musicreater.builtin_plugins.midi_read import (
|
||||
MIDI_DEFAULT_VOLUME_VALUE,
|
||||
MIDI_DEFAULT_PROGRAM_VALUE,
|
||||
volume_2_distance_straight as velocity_2_distance_straight,
|
||||
volume_2_distance_natural as velocity_2_distance_natural,
|
||||
panning_2_rotation_linear,
|
||||
panning_2_rotation_trigonometric,
|
||||
MM_CLASSIC_PITCHED_INSTRUMENT_TABLE,
|
||||
MM_CLASSIC_PERCUSSION_INSTRUMENT_TABLE,
|
||||
MM_DEFAULT_PITCHED_INSTRUMENT_TABLE,
|
||||
MM_DEFAULT_PERCUSSION_INSTRUMENT_TABLE,
|
||||
MM_DISLINK_PITCHED_INSTRUMENT_TABLE,
|
||||
MM_DISLINK_PERCUSSION_INSTRUMENT_TABLE,
|
||||
MM_NBS_PITCHED_INSTRUMENT_TABLE,
|
||||
MM_NBS_PERCUSSION_INSTRUMENT_TABLE,
|
||||
)
|
||||
|
||||
from Musicreater.constants import (
|
||||
# MIDI 表
|
||||
MIDI_PITCH_NAME_TABLE,
|
||||
MIDI_PITCHED_NOTE_NAME_GROUP,
|
||||
@@ -133,12 +145,4 @@ from .constants import (
|
||||
# MIDI 到 我的世界 表
|
||||
MM_INSTRUMENT_RANGE_TABLE,
|
||||
MM_INSTRUMENT_DEVIATION_TABLE,
|
||||
MM_CLASSIC_PITCHED_INSTRUMENT_TABLE,
|
||||
MM_CLASSIC_PERCUSSION_INSTRUMENT_TABLE,
|
||||
MM_TOUCH_PITCHED_INSTRUMENT_TABLE,
|
||||
MM_TOUCH_PERCUSSION_INSTRUMENT_TABLE,
|
||||
MM_DISLINK_PITCHED_INSTRUMENT_TABLE,
|
||||
MM_DISLINK_PERCUSSION_INSTRUMENT_TABLE,
|
||||
MM_NBS_PITCHED_INSTRUMENT_TABLE,
|
||||
MM_NBS_PERCUSSION_INSTRUMENT_TABLE,
|
||||
)
|
||||
|
||||
@@ -36,11 +36,41 @@ from itertools import chain
|
||||
|
||||
import mido
|
||||
|
||||
from .constants import *
|
||||
from Musicreater.constants import *
|
||||
from Musicreater.exceptions import (
|
||||
IllegalMinimumVolumeError,
|
||||
NoteBinaryFileVerificationFailed as MusicSequenceVerificationFailed,
|
||||
SingleNoteDecodeError,
|
||||
NoteBinaryFileTypeError as MusicSequenceTypeError,
|
||||
ZeroSpeedError,
|
||||
)
|
||||
|
||||
from Musicreater.builtin_plugins.midi_read.constants import (
|
||||
MIDI_DEFAULT_PROGRAM_VALUE,
|
||||
MIDI_DEFAULT_VOLUME_VALUE,
|
||||
MM_DEFAULT_PERCUSSION_INSTRUMENT_TABLE,
|
||||
MM_DEFAULT_PITCHED_INSTRUMENT_TABLE,
|
||||
)
|
||||
from Musicreater.builtin_plugins.midi_read.exceptions import (
|
||||
NoteOnOffMismatchError,
|
||||
LyricMismatchError,
|
||||
)
|
||||
from Musicreater.builtin_plugins.midi_read.utils import (
|
||||
volume_2_distance_natural,
|
||||
panning_2_rotation_trigonometric,
|
||||
panning_2_rotation_linear,
|
||||
)
|
||||
|
||||
from Musicreater.builtin_plugins.to_commands.progressbar import (
|
||||
DEFAULT_PROGRESSBAR_STYLE,
|
||||
ProgressBarStyle,
|
||||
)
|
||||
|
||||
|
||||
from .old_exceptions import *
|
||||
from .subclass import *
|
||||
from .old_types import *
|
||||
from .utils import *
|
||||
from .old_utils import *
|
||||
|
||||
"""
|
||||
学习笔记:
|
||||
@@ -75,6 +105,10 @@ tick * tempo / 1000000.0 / ticks_per_beat * 一秒多少游戏刻
|
||||
|
||||
"""
|
||||
|
||||
MIDI_PAN = "midi-pan"
|
||||
MIDI_PROGRAM = "midi-program"
|
||||
MIDI_VOLUME = "midi-volume"
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class MusicSequence:
|
||||
@@ -164,10 +198,10 @@ class MusicSequence:
|
||||
default_midi_program: int = MIDI_DEFAULT_PROGRAM_VALUE,
|
||||
default_midi_volume: int = MIDI_DEFAULT_VOLUME_VALUE,
|
||||
default_tempo: int = mido.midifiles.midifiles.DEFAULT_TEMPO,
|
||||
pitched_note_referance_table: MidiInstrumentTableType = MM_TOUCH_PITCHED_INSTRUMENT_TABLE,
|
||||
percussion_note_referance_table: MidiInstrumentTableType = MM_TOUCH_PERCUSSION_INSTRUMENT_TABLE,
|
||||
pitched_note_referance_table: MidiInstrumentTableType = MM_DEFAULT_PITCHED_INSTRUMENT_TABLE,
|
||||
percussion_note_referance_table: MidiInstrumentTableType = MM_DEFAULT_PERCUSSION_INSTRUMENT_TABLE,
|
||||
minimum_vol: float = 0.1,
|
||||
volume_processing_function: FittingFunctionType = velocity_2_distance_natural,
|
||||
volume_processing_function: FittingFunctionType = volume_2_distance_natural,
|
||||
panning_processing_function: FittingFunctionType = panning_2_rotation_linear,
|
||||
deviation: float = 0,
|
||||
note_referance_table_replacement: Dict[str, str] = {},
|
||||
@@ -258,7 +292,7 @@ class MusicSequence:
|
||||
|
||||
if bytes_buffer_in[:4] in (b"MSQ!", b"MSQ$"):
|
||||
|
||||
note_format_v3 = bytes_buffer_in[0] == b"MSQ$"
|
||||
note_format_v3 = bytes_buffer_in[:4] == b"MSQ$"
|
||||
|
||||
group_1 = int.from_bytes(bytes_buffer_in[4:6], "big", signed=False)
|
||||
group_2 = int.from_bytes(bytes_buffer_in[6:8], "big", signed=False)
|
||||
@@ -270,7 +304,7 @@ class MusicSequence:
|
||||
8 : (stt_index := 8 + (group_1 >> 10))
|
||||
].decode("GB18030")
|
||||
|
||||
channels_: MineNoteChannelType = empty_midi_channels(default_staff=[])
|
||||
channels_: MineNoteChannelType = enumerated_stuffcopy_dictionary(staff=[])
|
||||
total_note_count = 0
|
||||
if verify:
|
||||
_header_index = stt_index
|
||||
@@ -307,9 +341,9 @@ class MusicSequence:
|
||||
stt_index = end_index
|
||||
except Exception as _err:
|
||||
# print(channels_)
|
||||
raise MusicSequenceDecodeError(
|
||||
_err, "当前全部通道数据:", channels_
|
||||
)
|
||||
raise SingleNoteDecodeError(
|
||||
"当前全部通道数据:", channels_
|
||||
) from _err
|
||||
if verify:
|
||||
if (
|
||||
_count_verify := xxh3_64(
|
||||
@@ -412,7 +446,17 @@ class MusicSequence:
|
||||
_t6_buffer = _t2_buffer = 0
|
||||
|
||||
_channel_inst_chart: Dict[str, Dict[str, int]] = {}
|
||||
channels_: MineNoteChannelType = empty_midi_channels(default_staff=[])
|
||||
"""
|
||||
乐器对应通道的表
|
||||
{
|
||||
乐器名: {
|
||||
"CNT": 当前通道的音符数量,
|
||||
"INDEX": 当前乐器在通道中的索引
|
||||
}
|
||||
}
|
||||
也就是说,一个通道可以对应多个乐器(多个乐器可能分配到同一个通道)
|
||||
"""
|
||||
channels_: MineNoteChannelType = enumerated_stuffcopy_dictionary(staff=[])
|
||||
|
||||
for i in range(total_note_count):
|
||||
if verify:
|
||||
@@ -465,21 +509,31 @@ class MusicSequence:
|
||||
stt_index = end_index
|
||||
except Exception as _err:
|
||||
# print(bytes_buffer_in[stt_index:end_index])
|
||||
raise MusicSequenceDecodeError(
|
||||
_err, "所截取的音符码:", bytes_buffer_in[stt_index:end_index]
|
||||
)
|
||||
raise SingleNoteDecodeError(
|
||||
"所截取的音符码:", bytes_buffer_in[stt_index:end_index]
|
||||
) from _err
|
||||
|
||||
# 按照乐器分配通道
|
||||
if _read_note.sound_name in _channel_inst_chart:
|
||||
# 如果本身已经有这个乐器了,那就直接加一个计数就可以了
|
||||
_channel_inst_chart[_read_note.sound_name]["CNT"] += 1
|
||||
else:
|
||||
# 如果没有这个乐器
|
||||
if len(_channel_inst_chart) >= 16:
|
||||
# 已经超过了 16 个乐器,当前通道数量是装不下的
|
||||
# 那就找一个音符数量最少的通道,把这个通道引用为
|
||||
# 当前这个乐器的通道
|
||||
_channel_inst_chart[_read_note.sound_name] = min(
|
||||
_channel_inst_chart.values(), key=lambda x: x["CNT"]
|
||||
) # 此处是指针式内存引用
|
||||
_channel_inst_chart[_read_note.sound_name] = {
|
||||
"CNT": 1,
|
||||
"INDEX": len(_channel_inst_chart),
|
||||
}
|
||||
_channel_inst_chart[_read_note.sound_name]["CNT"] += 1
|
||||
else:
|
||||
# 没有超过 16 个乐器,那就加!
|
||||
_channel_inst_chart[_read_note.sound_name] = {
|
||||
"CNT": 1,
|
||||
"INDEX": len(_channel_inst_chart),
|
||||
}
|
||||
# 把音符添加到对应的通道里边
|
||||
channels_[_channel_inst_chart[_read_note.sound_name]["INDEX"]].append(
|
||||
_read_note
|
||||
)
|
||||
@@ -522,7 +576,7 @@ class MusicSequence:
|
||||
music_name_ = bytes_buffer_in[
|
||||
8 : (stt_index := 8 + (group_1 >> 10))
|
||||
].decode("GB18030")
|
||||
channels_: MineNoteChannelType = empty_midi_channels(default_staff=[])
|
||||
channels_: MineNoteChannelType = enumerated_stuffcopy_dictionary(staff=[])
|
||||
for channel_index in channels_.keys():
|
||||
for i in range(
|
||||
int.from_bytes(
|
||||
@@ -565,7 +619,7 @@ class MusicSequence:
|
||||
music_name_ = bytes_buffer_in[
|
||||
8 : (stt_index := 8 + (group_1 >> 10))
|
||||
].decode("utf-8")
|
||||
channels_: MineNoteChannelType = empty_midi_channels(default_staff=[])
|
||||
channels_: MineNoteChannelType = enumerated_stuffcopy_dictionary(staff=[])
|
||||
for channel_index in channels_.keys():
|
||||
for i in range(
|
||||
int.from_bytes(
|
||||
@@ -796,7 +850,7 @@ class MusicSequence:
|
||||
def add_note(self, channel_no: int, note: MineNote, is_sort: bool = True):
|
||||
"""
|
||||
在指定通道添加一个音符
|
||||
值得注意:在版本 2.2.3 及之前 is_sort 参数默认为 False ;在此之后为 True
|
||||
值得注意:在版本 2.2.3 及之前 is_sort 参数默认为 False;在此之后为 True
|
||||
"""
|
||||
self.channels[channel_no].append(note)
|
||||
self.total_note_count += 1
|
||||
@@ -815,9 +869,9 @@ class MusicSequence:
|
||||
default_program_value: int = MIDI_DEFAULT_PROGRAM_VALUE,
|
||||
default_volume_value: int = MIDI_DEFAULT_VOLUME_VALUE,
|
||||
default_tempo_value: int = mido.midifiles.midifiles.DEFAULT_TEMPO,
|
||||
pitched_note_rtable: MidiInstrumentTableType = MM_TOUCH_PITCHED_INSTRUMENT_TABLE,
|
||||
percussion_note_rtable: MidiInstrumentTableType = MM_TOUCH_PERCUSSION_INSTRUMENT_TABLE,
|
||||
vol_processing_function: FittingFunctionType = velocity_2_distance_natural,
|
||||
pitched_note_rtable: MidiInstrumentTableType = MM_DEFAULT_PITCHED_INSTRUMENT_TABLE,
|
||||
percussion_note_rtable: MidiInstrumentTableType = MM_DEFAULT_PERCUSSION_INSTRUMENT_TABLE,
|
||||
vol_processing_function: FittingFunctionType = volume_2_distance_natural,
|
||||
pan_processing_function: FittingFunctionType = panning_2_rotation_trigonometric,
|
||||
note_rtable_replacement: Dict[str, str] = {},
|
||||
) -> Tuple[MineNoteChannelType, int, Dict[str, int]]:
|
||||
@@ -857,10 +911,10 @@ class MusicSequence:
|
||||
raise ZeroSpeedError("播放速度不得为零,应为 (0,1] 范围内的实数。")
|
||||
|
||||
# 一个midi中仅有16个通道 我们通过通道来识别而不是音轨
|
||||
midi_channels: MineNoteChannelType = empty_midi_channels(default_staff=[])
|
||||
midi_channels: MineNoteChannelType = enumerated_stuffcopy_dictionary(staff=[])
|
||||
|
||||
channel_controler: Dict[int, Dict[str, int]] = empty_midi_channels(
|
||||
default_staff={
|
||||
channel_controler: Dict[int, Dict[str, int]] = enumerated_stuffcopy_dictionary(
|
||||
staff={
|
||||
MIDI_PROGRAM: default_program_value,
|
||||
MIDI_VOLUME: default_volume_value,
|
||||
MIDI_PAN: 64,
|
||||
@@ -880,7 +934,7 @@ class MusicSequence:
|
||||
int,
|
||||
]
|
||||
],
|
||||
] = empty_midi_channels(default_staff=[])
|
||||
] = enumerated_stuffcopy_dictionary(staff=[])
|
||||
note_queue_B: Dict[
|
||||
int,
|
||||
List[
|
||||
@@ -889,7 +943,7 @@ class MusicSequence:
|
||||
int,
|
||||
]
|
||||
],
|
||||
] = empty_midi_channels(default_staff=[])
|
||||
] = enumerated_stuffcopy_dictionary(staff=[])
|
||||
|
||||
lyric_cache: List[Tuple[int, str]] = []
|
||||
|
||||
@@ -970,7 +1024,7 @@ class MusicSequence:
|
||||
|
||||
# 更新结果信息
|
||||
midi_channels[msg.channel].append(
|
||||
that_note := midi_msgs_to_minenote(
|
||||
that_note := midi_msgs_to_minenote( # 无法强行兼容了,pass
|
||||
inst_=(
|
||||
msg.note
|
||||
if (_is_percussion := (msg.channel == 9))
|
||||
@@ -1092,11 +1146,11 @@ class MidiConvert(MusicSequence):
|
||||
default_midi_program_value: int = MIDI_DEFAULT_PROGRAM_VALUE,
|
||||
default_midi_volume_value: int = MIDI_DEFAULT_VOLUME_VALUE,
|
||||
default_tempo_value: int = mido.midifiles.midifiles.DEFAULT_TEMPO,
|
||||
pitched_note_rtable: MidiInstrumentTableType = MM_TOUCH_PITCHED_INSTRUMENT_TABLE,
|
||||
percussion_note_rtable: MidiInstrumentTableType = MM_TOUCH_PERCUSSION_INSTRUMENT_TABLE,
|
||||
pitched_note_rtable: MidiInstrumentTableType = MM_DEFAULT_PITCHED_INSTRUMENT_TABLE,
|
||||
percussion_note_rtable: MidiInstrumentTableType = MM_DEFAULT_PERCUSSION_INSTRUMENT_TABLE,
|
||||
enable_old_exe_format: bool = False,
|
||||
minimum_volume: float = 0.1,
|
||||
vol_processing_function: FittingFunctionType = velocity_2_distance_natural,
|
||||
vol_processing_function: FittingFunctionType = volume_2_distance_natural,
|
||||
pan_processing_function: FittingFunctionType = panning_2_rotation_trigonometric,
|
||||
pitch_deviation: float = 0,
|
||||
note_rtable_replacement: Dict[str, str] = {},
|
||||
@@ -1175,11 +1229,11 @@ class MidiConvert(MusicSequence):
|
||||
default_midi_program: int = MIDI_DEFAULT_PROGRAM_VALUE,
|
||||
default_midi_volume: int = MIDI_DEFAULT_VOLUME_VALUE,
|
||||
default_tempo: int = mido.midifiles.midifiles.DEFAULT_TEMPO,
|
||||
pitched_note_table: MidiInstrumentTableType = MM_TOUCH_PITCHED_INSTRUMENT_TABLE,
|
||||
percussion_note_table: MidiInstrumentTableType = MM_TOUCH_PERCUSSION_INSTRUMENT_TABLE,
|
||||
pitched_note_table: MidiInstrumentTableType = MM_DEFAULT_PITCHED_INSTRUMENT_TABLE,
|
||||
percussion_note_table: MidiInstrumentTableType = MM_DEFAULT_PERCUSSION_INSTRUMENT_TABLE,
|
||||
old_exe_format: bool = False,
|
||||
min_volume: float = 0.1,
|
||||
vol_processing_func: FittingFunctionType = velocity_2_distance_natural,
|
||||
vol_processing_func: FittingFunctionType = volume_2_distance_natural,
|
||||
pan_processing_func: FittingFunctionType = panning_2_rotation_linear,
|
||||
music_pitch_deviation: float = 0,
|
||||
note_table_replacement: Dict[str, str] = {},
|
||||
@@ -1274,7 +1328,7 @@ class MidiConvert(MusicSequence):
|
||||
-------
|
||||
list[MineCommand,]
|
||||
"""
|
||||
pgs_style = progressbar_style.base_style
|
||||
pgs_style = progressbar_style.style_base_string
|
||||
"""用于被替换的进度条原始样式"""
|
||||
|
||||
"""
|
||||
@@ -1459,8 +1513,8 @@ class MidiConvert(MusicSequence):
|
||||
|
||||
for i in range(pgs_style.count("_")):
|
||||
npg_stl = (
|
||||
pgs_style.replace("_", progressbar_style.played_style, i + 1)
|
||||
.replace("_", progressbar_style.to_play_style)
|
||||
pgs_style.replace("_", progressbar_style.progress_played, i + 1)
|
||||
.replace("_", progressbar_style.progress_toplay)
|
||||
.replace(r"%%N", self.music_name)
|
||||
.replace(
|
||||
r"%%s",
|
||||
@@ -1700,7 +1754,7 @@ class MidiConvert(MusicSequence):
|
||||
"{}:{:.2f}".format(mc_sound_ID, mc_pitch),
|
||||
)
|
||||
),
|
||||
tick_delay=tickdelay,
|
||||
delay=tickdelay,
|
||||
),
|
||||
)
|
||||
delaytime_previous = note.start_tick
|
||||
@@ -1786,7 +1840,7 @@ class MidiConvert(MusicSequence):
|
||||
"{}:{:.2f}".format(mc_sound_ID, mc_pitch),
|
||||
)
|
||||
),
|
||||
tick_delay=tickdelay,
|
||||
delay=tickdelay,
|
||||
),
|
||||
)
|
||||
delaytime_previous[note.sound_name] = note.start_tick
|
||||
|
||||
@@ -99,7 +99,7 @@ def to_addon_pack_in_score(
|
||||
"w",
|
||||
encoding="utf-8",
|
||||
) as f:
|
||||
f.write("\n".join([single_cmd.cmd for single_cmd in cmdlist[i]]))
|
||||
f.write("\n".join([single_cmd.mcfunction_command_string for single_cmd in cmdlist[i]]))
|
||||
index_file.writelines(
|
||||
(
|
||||
"scoreboard players add @a[scores={"
|
||||
@@ -132,7 +132,7 @@ def to_addon_pack_in_score(
|
||||
f.writelines(
|
||||
"\n".join(
|
||||
[
|
||||
single_cmd.cmd
|
||||
single_cmd.mcfunction_command_string
|
||||
for single_cmd in midi_cvt.form_progress_bar(
|
||||
maxscore, scoreboard_name, progressbar_style
|
||||
)
|
||||
|
||||
@@ -16,7 +16,7 @@ from typing import Literal
|
||||
|
||||
from ...old_main import MidiConvert
|
||||
from ...subclass import MineCommand
|
||||
from ..mcstructure import (
|
||||
from Musicreater.builtin_plugins.commands_to_structure.mcstructure import (
|
||||
COMPABILITY_VERSION_117,
|
||||
COMPABILITY_VERSION_119,
|
||||
commands_to_redstone_delay_structure,
|
||||
|
||||
@@ -98,8 +98,8 @@ def to_websocket_server(
|
||||
"title {} actionbar {}".format(
|
||||
whom_to_play,
|
||||
progressbar_style.play_output(
|
||||
played_delays=i,
|
||||
total_delays=musics[music_to_play][1],
|
||||
played_ticks=i,
|
||||
total_ticks=musics[music_to_play][1],
|
||||
music_name=music_to_play,
|
||||
),
|
||||
),
|
||||
@@ -111,7 +111,7 @@ def to_websocket_server(
|
||||
>= (cmd := musics[music_to_play][0][now_played_cmd]).delay
|
||||
):
|
||||
await self.send_command(
|
||||
cmd.command_text.replace(replacement, whom_to_play),
|
||||
cmd.command.replace(replacement, whom_to_play),
|
||||
callback=self.cmd_feedback,
|
||||
)
|
||||
now_played_cmd += 1
|
||||
|
||||
@@ -34,37 +34,67 @@ from typing import (
|
||||
|
||||
from xxhash import xxh3_64, xxh3_128, xxh32
|
||||
|
||||
from .constants import (
|
||||
from Musicreater.constants import (
|
||||
MC_INSTRUMENT_BLOCKS_TABLE,
|
||||
MC_PITCHED_INSTRUMENT_LIST,
|
||||
MM_INSTRUMENT_DEVIATION_TABLE,
|
||||
MM_INSTRUMENT_RANGE_TABLE,
|
||||
)
|
||||
from .old_exceptions import MusicSequenceDecodeError
|
||||
from Musicreater.exceptions import SingleNoteDecodeError
|
||||
from Musicreater._utils import enumerated_stuffcopy_dictionary
|
||||
|
||||
from Musicreater.builtin_plugins.midi_read.utils import midi_inst_to_mc_sound
|
||||
|
||||
from .subclass import MineNote, mctick2timestr, SingleNoteBox
|
||||
from .old_types import MidiInstrumentTableType, MineNoteChannelType, FittingFunctionType
|
||||
|
||||
|
||||
def empty_midi_channels(
|
||||
channel_count: int = 17, default_staff: Any = {}
|
||||
) -> Dict[int, Any]:
|
||||
"""
|
||||
空MIDI通道字典
|
||||
"""
|
||||
|
||||
return dict(
|
||||
(
|
||||
i,
|
||||
(
|
||||
default_staff.copy()
|
||||
if isinstance(default_staff, (dict, list))
|
||||
else default_staff
|
||||
),
|
||||
) # 这告诉我们,你不能忽略任何一个复制的序列,因为它真的,我哭死,折磨我一整天,全在这个bug上了
|
||||
for i in range(channel_count)
|
||||
def volume_2_distance_natural(
|
||||
vol: float,
|
||||
) -> float:
|
||||
"""
|
||||
Midi 力度值/音量值拟合成的距离函数,一种更加自然的听感?
|
||||
|
||||
Parameters
|
||||
----------
|
||||
vol: int
|
||||
Midi 音符力度值(0~127)
|
||||
|
||||
Returns
|
||||
-------
|
||||
float播放中心到玩家的距离
|
||||
"""
|
||||
return (
|
||||
-8.081720684086314
|
||||
* math.log(
|
||||
vol + 14.579508825070013,
|
||||
)
|
||||
+ 37.65806375944386
|
||||
if vol < 60.64
|
||||
else 0.2721359356095803 * ((vol + 2592.272889454798) ** 1.358571233418649)
|
||||
+ -6.313841334963396 * (vol + 2592.272889454798)
|
||||
+ 4558.496367823575
|
||||
)
|
||||
|
||||
|
||||
def volume_2_distance_straight(vol: float) -> float:
|
||||
"""
|
||||
Midi 力度值/音量值拟合成的距离函数,线性转换
|
||||
|
||||
Parameters
|
||||
----------
|
||||
vol: int
|
||||
Midi 音符力度值(0~127)
|
||||
|
||||
Returns
|
||||
-------
|
||||
float播放中心到玩家的距离
|
||||
"""
|
||||
return (vol + 1) / -8 + 16
|
||||
|
||||
|
||||
|
||||
def inst_to_sould_with_deviation(
|
||||
instrumentID: int,
|
||||
reference_table: MidiInstrumentTableType,
|
||||
@@ -100,243 +130,7 @@ def inst_to_sould_with_deviation(
|
||||
)
|
||||
|
||||
|
||||
def midi_inst_to_mc_sound(
|
||||
instrumentID: int,
|
||||
reference_table: MidiInstrumentTableType,
|
||||
default_instrument: str = "note.flute",
|
||||
) -> str:
|
||||
"""
|
||||
返回midi的乐器ID对应的我的世界乐器名
|
||||
|
||||
Parameters
|
||||
----------
|
||||
instrumentID: int
|
||||
midi的乐器ID
|
||||
reference_table: Dict[int, Tuple[str, int]]
|
||||
转换乐器参照表
|
||||
default_instrument: str
|
||||
查无此乐器时的替换乐器
|
||||
|
||||
Returns
|
||||
-------
|
||||
str我的世界乐器名
|
||||
"""
|
||||
return reference_table.get(
|
||||
instrumentID,
|
||||
default_instrument,
|
||||
)
|
||||
|
||||
|
||||
def velocity_2_distance_natural(
|
||||
vol: float,
|
||||
) -> float:
|
||||
"""
|
||||
midi力度值拟合成的距离函数
|
||||
|
||||
Parameters
|
||||
----------
|
||||
vol: int
|
||||
midi 音符力度值
|
||||
|
||||
Returns
|
||||
-------
|
||||
float播放中心到玩家的距离
|
||||
"""
|
||||
return (
|
||||
-8.081720684086314
|
||||
* math.log(
|
||||
vol + 14.579508825070013,
|
||||
)
|
||||
+ 37.65806375944386
|
||||
if vol < 60.64
|
||||
else 0.2721359356095803 * ((vol + 2592.272889454798) ** 1.358571233418649)
|
||||
+ -6.313841334963396 * (vol + 2592.272889454798)
|
||||
+ 4558.496367823575
|
||||
)
|
||||
|
||||
|
||||
def velocity_2_distance_straight(vol: float) -> float:
|
||||
"""
|
||||
midi力度值拟合成的距离函数
|
||||
|
||||
Parameters
|
||||
----------
|
||||
vol: int
|
||||
midi 音符力度值
|
||||
|
||||
Returns
|
||||
-------
|
||||
float播放中心到玩家的距离
|
||||
"""
|
||||
return vol / -8 + 16
|
||||
|
||||
|
||||
def panning_2_rotation_linear(pan_: float) -> float:
|
||||
"""
|
||||
Midi 左右平衡偏移值线性转为声源旋转角度
|
||||
|
||||
Parameters
|
||||
----------
|
||||
pan_: int
|
||||
Midi 左右平衡偏移值
|
||||
注:此参数为int,范围从 0 到 127,当为 64 时,声源居中
|
||||
|
||||
Returns
|
||||
-------
|
||||
float
|
||||
声源旋转角度
|
||||
"""
|
||||
return (pan_ - 64) * 90 / 63
|
||||
|
||||
|
||||
def panning_2_rotation_trigonometric(pan_: float) -> float:
|
||||
"""
|
||||
Midi 左右平衡偏移值,依照圆的声场定位,转为声源旋转角度
|
||||
|
||||
Parameters
|
||||
----------
|
||||
pan_: int
|
||||
Midi 左右平衡偏移值
|
||||
注:此参数为int,范围从 0 到 127,当为 64 时,声源居中
|
||||
|
||||
Returns
|
||||
-------
|
||||
float
|
||||
声源旋转角度
|
||||
"""
|
||||
if pan_ <= 0:
|
||||
return -90
|
||||
elif pan_ >= 127:
|
||||
return 90
|
||||
else:
|
||||
return math.degrees(math.acos((64 - pan_) / 63)) - 90
|
||||
|
||||
|
||||
def minenote_to_command_parameters(
|
||||
mine_note: MineNote,
|
||||
pitch_deviation: float = 0,
|
||||
) -> Tuple[
|
||||
str,
|
||||
Tuple[float, float, float],
|
||||
float,
|
||||
Union[float, Literal[None]],
|
||||
]:
|
||||
"""
|
||||
将 MineNote 对象转为《我的世界》音符播放所需之参数
|
||||
|
||||
Parameters
|
||||
------------
|
||||
mine_note: MineNote
|
||||
音符对象
|
||||
deviation: float
|
||||
音调偏移量
|
||||
|
||||
Returns
|
||||
---------
|
||||
str, tuple[float, float, float], float, float
|
||||
我的世界音符ID, 播放视角坐标, 指令音量参数, 指令音调参数
|
||||
"""
|
||||
|
||||
return (
|
||||
mine_note.sound_name,
|
||||
mine_note.position_displacement,
|
||||
mine_note.velocity / 127,
|
||||
(
|
||||
None
|
||||
if mine_note.percussive
|
||||
else (
|
||||
2
|
||||
** (
|
||||
(
|
||||
mine_note.note_pitch
|
||||
- 60
|
||||
- MM_INSTRUMENT_DEVIATION_TABLE.get(mine_note.sound_name, 6)
|
||||
+ pitch_deviation
|
||||
)
|
||||
/ 12
|
||||
)
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def midi_msgs_to_minenote(
|
||||
inst_: int, # 乐器编号
|
||||
note_: int,
|
||||
percussive_: bool, # 是否作为打击乐器启用
|
||||
volume_: int,
|
||||
velocity_: int,
|
||||
panning_: int,
|
||||
start_time_: int,
|
||||
duration_: int,
|
||||
play_speed: float,
|
||||
midi_reference_table: MidiInstrumentTableType,
|
||||
volume_processing_method_: FittingFunctionType,
|
||||
panning_processing_method_: FittingFunctionType,
|
||||
note_table_replacement: Dict[str, str] = {},
|
||||
lyric_line: str = "",
|
||||
) -> MineNote:
|
||||
"""
|
||||
将Midi信息转为我的世界音符对象
|
||||
|
||||
Parameters
|
||||
------------
|
||||
inst_: int
|
||||
乐器编号
|
||||
note_: int
|
||||
音高编号(音符编号)
|
||||
percussive_: bool
|
||||
是否作为打击乐器启用
|
||||
volume_: int
|
||||
音量
|
||||
velocity_: int
|
||||
力度
|
||||
panning_: int
|
||||
声相偏移
|
||||
start_time_: int
|
||||
音符起始时间(微秒)
|
||||
duration_: int
|
||||
音符持续时间(微秒)
|
||||
play_speed: float
|
||||
曲目播放速度
|
||||
midi_reference_table: Dict[int, str]
|
||||
转换对照表
|
||||
volume_processing_method_: Callable[[float], float]
|
||||
音量处理函数
|
||||
panning_processing_method_: Callable[[float], float]
|
||||
立体声相偏移处理函数
|
||||
note_table_replacement: Dict[str, str]
|
||||
音符替换表,定义 Minecraft 音符字串的替换
|
||||
lyric_line: str
|
||||
该音符的歌词
|
||||
|
||||
Returns
|
||||
---------
|
||||
MineNote
|
||||
我的世界音符对象
|
||||
"""
|
||||
mc_sound_ID = midi_inst_to_mc_sound(
|
||||
inst_,
|
||||
midi_reference_table,
|
||||
"note.bd" if percussive_ else "note.flute",
|
||||
)
|
||||
|
||||
return MineNote(
|
||||
mc_sound_name=note_table_replacement.get(mc_sound_ID, mc_sound_ID),
|
||||
midi_pitch=note_,
|
||||
midi_velocity=velocity_,
|
||||
start_time=(tk := int(start_time_ / float(play_speed) / 50000)),
|
||||
last_time=round(duration_ / float(play_speed) / 50000),
|
||||
mass_precision_time=round((start_time_ / float(play_speed) - tk * 50000) / 800),
|
||||
is_percussion=percussive_,
|
||||
distance=volume_processing_method_(volume_),
|
||||
azimuth=(panning_processing_method_(panning_), 0),
|
||||
extra_information={
|
||||
"LYRIC_TEXT": lyric_line,
|
||||
"VOLUME_VALUE": volume_,
|
||||
"PIN_VALUE": panning_,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def midi_msgs_to_minenote_using_kami_respack(
|
||||
@@ -634,11 +428,10 @@ def load_decode_fsq_flush_release(
|
||||
)
|
||||
except Exception as _err:
|
||||
# print(bytes_buffer_in[stt_index:end_index])
|
||||
raise MusicSequenceDecodeError(
|
||||
_err,
|
||||
raise SingleNoteDecodeError(
|
||||
"所截取的音符码之首个字节:",
|
||||
_first_byte,
|
||||
)
|
||||
) from _err
|
||||
|
||||
|
||||
def load_decode_msq_flush_release(
|
||||
@@ -685,8 +478,8 @@ def load_decode_msq_flush_release(
|
||||
|
||||
_total_note_count = 1
|
||||
|
||||
_channel_infos = empty_midi_channels(
|
||||
default_staff={"NOW_INDEX": 0, "NOTE_COUNT": 0, "HAVE_READ": 0, "END_INDEX": -1}
|
||||
_channel_infos = enumerated_stuffcopy_dictionary(
|
||||
staff={"NOW_INDEX": 0, "NOTE_COUNT": 0, "HAVE_READ": 0, "END_INDEX": -1}
|
||||
)
|
||||
|
||||
for __channel_index in _channel_infos.keys():
|
||||
@@ -815,7 +608,7 @@ def load_decode_msq_flush_release(
|
||||
_total_note_count -= 1
|
||||
except Exception as _err:
|
||||
# print(channels_)
|
||||
raise MusicSequenceDecodeError("难以定位的解码错误", _err)
|
||||
raise SingleNoteDecodeError("难以定位的解码错误") from _err
|
||||
if not _read_in_note_list:
|
||||
break
|
||||
# _note_list.append
|
||||
@@ -20,7 +20,13 @@ from math import sin, cos, asin, radians, degrees, sqrt, atan
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional, Any, List, Tuple, Union, Dict, Sequence
|
||||
|
||||
from .constants import MC_PITCHED_INSTRUMENT_LIST
|
||||
from Musicreater.constants import MC_PITCHED_INSTRUMENT_LIST
|
||||
from Musicreater.builtin_plugins.to_commands.main import MineCommand
|
||||
from Musicreater.builtin_plugins.to_commands.progressbar import (
|
||||
ProgressBarStyle,
|
||||
mctick2timestr,
|
||||
DEFAULT_PROGRESSBAR_STYLE,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
@@ -525,80 +531,6 @@ class MineNote:
|
||||
return self.tuplize() == other.tuplize()
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class MineCommand:
|
||||
"""存储单个指令的类"""
|
||||
|
||||
command_text: str
|
||||
"""指令文本"""
|
||||
|
||||
conditional: bool
|
||||
"""执行是否有条件"""
|
||||
|
||||
delay: int
|
||||
"""执行的延迟"""
|
||||
|
||||
annotation_text: str
|
||||
"""指令注释"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
command: str,
|
||||
condition: bool = False,
|
||||
tick_delay: int = 0,
|
||||
annotation: str = "",
|
||||
):
|
||||
"""
|
||||
存储单个指令的类
|
||||
|
||||
Parameters
|
||||
----------
|
||||
command: str
|
||||
指令
|
||||
condition: bool
|
||||
是否有条件
|
||||
tick_delay: int
|
||||
执行延时
|
||||
annotation: str
|
||||
注释
|
||||
"""
|
||||
self.command_text = command
|
||||
self.conditional = condition
|
||||
self.delay = tick_delay
|
||||
self.annotation_text = annotation
|
||||
|
||||
def copy(self):
|
||||
return MineCommand(
|
||||
command=self.command_text,
|
||||
condition=self.conditional,
|
||||
tick_delay=self.delay,
|
||||
annotation=self.annotation_text,
|
||||
)
|
||||
|
||||
@property
|
||||
def cmd(self) -> str:
|
||||
"""
|
||||
我的世界函数字符串(包含注释)
|
||||
"""
|
||||
return self.__str__()
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""
|
||||
转为我的世界函数文件格式(包含注释)
|
||||
"""
|
||||
return "# {cdt}<{delay}> {ant}\n{cmd}".format(
|
||||
cdt="[CDT]" if self.conditional else "",
|
||||
delay=self.delay,
|
||||
ant=self.annotation_text,
|
||||
cmd=self.command_text,
|
||||
)
|
||||
|
||||
def __eq__(self, other) -> bool:
|
||||
if not isinstance(other, self.__class__):
|
||||
return False
|
||||
return self.__str__() == other.__str__()
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class SingleNoteBox:
|
||||
"""存储单个音符盒"""
|
||||
@@ -704,158 +636,3 @@ class SingleNoteBox:
|
||||
if not isinstance(other, self.__class__):
|
||||
return False
|
||||
return self.__str__() == other.__str__()
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class ProgressBarStyle:
|
||||
"""进度条样式类"""
|
||||
|
||||
base_style: str
|
||||
"""基础样式"""
|
||||
|
||||
to_play_style: str
|
||||
"""未播放之样式"""
|
||||
|
||||
played_style: str
|
||||
"""已播放之样式"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
base_s: Optional[str] = None,
|
||||
to_play_s: Optional[str] = None,
|
||||
played_s: Optional[str] = None,
|
||||
):
|
||||
"""
|
||||
用于存储进度条样式的类
|
||||
|
||||
| 标识符 | 指定的可变量 |
|
||||
|---------|----------------|
|
||||
| `%%N` | 乐曲名(即传入的文件名)|
|
||||
| `%%s` | 当前计分板值 |
|
||||
| `%^s` | 计分板最大值 |
|
||||
| `%%t` | 当前播放时间 |
|
||||
| `%^t` | 曲目总时长 |
|
||||
| `%%%` | 当前进度比率 |
|
||||
| `_` | 用以表示进度条占位|
|
||||
|
||||
Parameters
|
||||
------------
|
||||
base_s: str
|
||||
基础样式,用以定义进度条整体
|
||||
to_play_s: str
|
||||
进度条样式:尚未播放的样子
|
||||
played_s: str
|
||||
已经播放的样子
|
||||
|
||||
Returns
|
||||
---------
|
||||
ProgressBarStyle 类
|
||||
"""
|
||||
|
||||
self.base_style = (
|
||||
base_s if base_s else r"▶ %%N [ %%s/%^s %%% §e__________§r %%t|%^t ]"
|
||||
)
|
||||
self.to_play_style = to_play_s if to_play_s else r"§7="
|
||||
self.played_style = played_s if played_s else r"="
|
||||
|
||||
@classmethod
|
||||
def from_tuple(cls, tuplized_style: Optional[Tuple[str, Tuple[str, str]]]):
|
||||
"""自旧版进度条元组表示法读入数据(已不建议使用)"""
|
||||
|
||||
if tuplized_style is None:
|
||||
return cls(
|
||||
r"▶ %%N [ %%s/%^s %%% §e__________§r %%t|%^t ]",
|
||||
r"§7=",
|
||||
r"=",
|
||||
)
|
||||
|
||||
if isinstance(tuplized_style, tuple):
|
||||
if isinstance(tuplized_style[0], str) and isinstance(
|
||||
tuplized_style[1], tuple
|
||||
):
|
||||
if isinstance(tuplized_style[1][0], str) and isinstance(
|
||||
tuplized_style[1][1], str
|
||||
):
|
||||
return cls(
|
||||
tuplized_style[0], tuplized_style[1][0], tuplized_style[1][1]
|
||||
)
|
||||
raise ValueError(
|
||||
"元组表示的进度条样式组 {} 格式错误,已不建议使用此功能,请尽快更换。".format(
|
||||
tuplized_style
|
||||
)
|
||||
)
|
||||
|
||||
def set_base_style(self, value: str):
|
||||
"""设置基础样式"""
|
||||
self.base_style = value
|
||||
|
||||
def set_to_play_style(self, value: str):
|
||||
"""设置未播放之样式"""
|
||||
self.to_play_style = value
|
||||
|
||||
def set_played_style(self, value: str):
|
||||
"""设置已播放之样式"""
|
||||
self.played_style = value
|
||||
|
||||
def copy(self):
|
||||
dst = ProgressBarStyle(self.base_style, self.to_play_style, self.played_style)
|
||||
return dst
|
||||
|
||||
def play_output(
|
||||
self,
|
||||
played_delays: int,
|
||||
total_delays: int,
|
||||
music_name: str = "无题",
|
||||
) -> str:
|
||||
"""
|
||||
直接依照此格式输出一个进度条
|
||||
|
||||
Parameters
|
||||
------------
|
||||
played_delays: int
|
||||
当前播放进度积分值
|
||||
total_delays: int
|
||||
乐器总延迟数(计分板值)
|
||||
music_name: str
|
||||
曲名
|
||||
|
||||
Returns
|
||||
---------
|
||||
str
|
||||
进度条字符串
|
||||
"""
|
||||
|
||||
return (
|
||||
self.base_style.replace(r"%%N", music_name)
|
||||
.replace(r"%%s", str(played_delays))
|
||||
.replace(r"%^s", str(total_delays))
|
||||
.replace(r"%%t", mctick2timestr(played_delays))
|
||||
.replace(r"%^t", mctick2timestr(total_delays))
|
||||
.replace(
|
||||
r"%%%",
|
||||
"{:0>5.2f}%".format(int(10000 * played_delays / total_delays) / 100),
|
||||
)
|
||||
.replace(
|
||||
"_",
|
||||
self.played_style,
|
||||
(played_delays * self.base_style.count("_") // total_delays) + 1,
|
||||
)
|
||||
.replace("_", self.to_play_style)
|
||||
)
|
||||
|
||||
|
||||
def mctick2timestr(mc_tick: int) -> str:
|
||||
"""
|
||||
将《我的世界》的游戏刻计转为表示时间的字符串
|
||||
"""
|
||||
return "{:0>2d}:{:0>2d}".format(mc_tick // 1200, (mc_tick // 20) % 60)
|
||||
|
||||
|
||||
DEFAULT_PROGRESSBAR_STYLE = ProgressBarStyle(
|
||||
r"▶ %%N [ %%s/%^s %%% §e__________§r %%t|%^t ]",
|
||||
r"§7=",
|
||||
r"=",
|
||||
)
|
||||
"""
|
||||
默认的进度条样式
|
||||
"""
|
||||
|
||||
@@ -71,7 +71,7 @@ def to_zip_pack_in_score(
|
||||
"w",
|
||||
encoding="utf-8",
|
||||
) as f:
|
||||
f.write("\n".join([single_cmd.cmd for single_cmd in cmdlist[i]]))
|
||||
f.write("\n".join([single_cmd.mcfunction_command_string for single_cmd in cmdlist[i]]))
|
||||
index_file.writelines(
|
||||
(
|
||||
"scoreboard players add @a[score_{0}_min=1] {0} 1\n".format(
|
||||
@@ -97,7 +97,7 @@ def to_zip_pack_in_score(
|
||||
f.writelines(
|
||||
"\n".join(
|
||||
[
|
||||
single_cmd.cmd
|
||||
single_cmd.mcfunction_command_string
|
||||
for single_cmd in midi_cvt.form_java_progress_bar(
|
||||
maxscore, scoreboard_name, progressbar_style
|
||||
)
|
||||
|
||||
+38
-10
@@ -3,10 +3,9 @@
|
||||
dynamic = ["version"]
|
||||
requires-python = ">= 3.8, < 4.0"
|
||||
dependencies = [
|
||||
"mido >= 1.3",
|
||||
"tomli >= 2.4.0; python_version < '3.11'",
|
||||
"tomli-w >= 1.0.0",
|
||||
"xxhash >= 3",
|
||||
"tomli >= 2.4.0, < 3.0 ; python_version < '3.11'",
|
||||
"tomli-w >= 1.0.0, < 2.0",
|
||||
"xxhash >= 3.0, < 4.0",
|
||||
]
|
||||
|
||||
authors = [
|
||||
@@ -19,7 +18,7 @@
|
||||
{ name = "金羿Eilles", email = "EillesWan@outlook.com" },
|
||||
]
|
||||
|
||||
description = "A free open source library used for dealing with **Minecraft** digital musics."
|
||||
description = "A blazing fast and plugin-supported library for editing _Minecraft_ digital music."
|
||||
readme = "README_EN.md"
|
||||
license = { file = "LICENSE.md" }
|
||||
|
||||
@@ -46,14 +45,43 @@
|
||||
|
||||
|
||||
[project.optional-dependencies]
|
||||
full = [
|
||||
midi = [
|
||||
"mido >= 1.3, < 2.0",
|
||||
]
|
||||
structure = [
|
||||
"numpy < 3.0",
|
||||
"TrimMCStruct <= 0.0.5.9",
|
||||
"brotli >= 1.0.0",
|
||||
"numpy",
|
||||
"brotli >= 1.0.0, < 2.0",
|
||||
]
|
||||
preview = [
|
||||
"numpy < 3.0",
|
||||
"soundfile",
|
||||
"sounddevice",
|
||||
"librosa",
|
||||
]
|
||||
preview-advanced = [
|
||||
"pyloudnorm",
|
||||
]
|
||||
full = [
|
||||
"mido >= 1.3, < 2.0",
|
||||
"numpy < 3.0",
|
||||
"TrimMCStruct <= 0.0.5.9",
|
||||
"brotli >= 1.0.0, < 2.0",
|
||||
"soundfile",
|
||||
"sounddevice",
|
||||
"librosa",
|
||||
"pyloudnorm",
|
||||
]
|
||||
dev = [
|
||||
"mido >= 1.3, < 2.0",
|
||||
"numpy < 3.0",
|
||||
"TrimMCStruct <= 0.0.5.9",
|
||||
"brotli >= 1.0.0",
|
||||
"brotli >= 1.0.0, < 2.0",
|
||||
"soundfile",
|
||||
"sounddevice",
|
||||
"librosa",
|
||||
"pyloudnorm",
|
||||
"matplotlib",
|
||||
"dill",
|
||||
"rich",
|
||||
"pyinstaller",
|
||||
@@ -98,4 +126,4 @@
|
||||
|
||||
|
||||
[tool.pyright]
|
||||
typeCheckingMode = "basic"
|
||||
typeCheckingMode = "standard"
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import os
|
||||
import numpy as np
|
||||
import soundfile as sf
|
||||
import pyloudnorm as pyln
|
||||
|
||||
def analyze_game_sfx(wav_dir: str) -> dict:
|
||||
"""
|
||||
批量分析游戏音效的听感响度(LUFS)。
|
||||
对于短音效,优先使用 Momentary Loudness;若文件过短则回退到 K-weighted RMS。
|
||||
"""
|
||||
results = {}
|
||||
|
||||
if not os.path.exists(wav_dir):
|
||||
print(f"错误: 目录不存在 -> {wav_dir}")
|
||||
return results
|
||||
|
||||
wav_files = [f for f in os.listdir(wav_dir) if f.lower().endswith('.wav')]
|
||||
print(f"找到 {len(wav_files)} 个 WAV 文件,开始分析...\n")
|
||||
|
||||
for filename in sorted(wav_files):
|
||||
filepath = os.path.join(wav_dir, filename)
|
||||
try:
|
||||
data, sr = sf.read(filepath)
|
||||
|
||||
# 确保为二维数组 (samples, channels)
|
||||
if data.ndim == 1:
|
||||
data = data.reshape(-1, 1)
|
||||
|
||||
duration = len(data) / sr
|
||||
|
||||
# --- 核心响度计算 ---
|
||||
# ITU-R BS.1770 要求至少 ~400ms 才能准确计算 Integrated Loudness
|
||||
# 游戏音效通常很短,我们采用以下策略:
|
||||
meter = pyln.Meter(sr, block_size=duration) # 400ms block
|
||||
|
||||
# if duration >= 0.4:
|
||||
# 使用 Momentary Loudness (无门限,适合短音效)
|
||||
loudness = meter.integrated_loudness(data)
|
||||
# else:
|
||||
# # 极短音效:手动 K-weighting 后计算 RMS → 转为 LUFS
|
||||
# # pyloudnorm 内部有 K-weight filter,这里用简化方式
|
||||
# filtered = meter._filter_k_weighting(data.T).T
|
||||
# rms = np.sqrt(np.mean(filtered ** 2))
|
||||
# # LUFS ≈ -0.691 + 10*log10(rms^2),参考 BS.1770 公式
|
||||
# loudness = -0.691 + 10 * np.log10(max(rms ** 2, 1e-12))
|
||||
# method = "K-weighted RMS"
|
||||
|
||||
# 静音检测
|
||||
if np.max(np.abs(data)) < 1e-6:
|
||||
loudness = -np.inf
|
||||
|
||||
results[filename] = {
|
||||
"loudness_lufs": round(loudness, 2),
|
||||
"duration_sec": round(duration, 4),
|
||||
"sample_rate": sr,
|
||||
"channels": data.shape[1],
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
results[filename] = {"error": str(e)}
|
||||
print(f" ⚠ 跳过 {filename}: {e}")
|
||||
|
||||
return results
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
TARGET_DIR = "vanilla_assets/wav/"
|
||||
report = analyze_game_sfx(TARGET_DIR)
|
||||
|
||||
# 打印结果字典
|
||||
print("\n===== 分析结果 =====")
|
||||
print("文件名 | 响度(LUFS) | 时长(s) | 采样率")
|
||||
for name, info in report.items():
|
||||
if "error" in info:
|
||||
print(f"{name}: 错误 - {info['error']}")
|
||||
else:
|
||||
print(f"{name}: {info['loudness_lufs']:>7.2f} LUFS | "
|
||||
f"{info['duration_sec']:.3f}s | {info['sample_rate']}Hz")
|
||||
|
||||
# 也输出原始字典方便程序化使用
|
||||
print("\n===== Raw Dict =====")
|
||||
print(report)
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"rawtext": [
|
||||
{
|
||||
"translate": "%%4",
|
||||
"with": {
|
||||
"rawtext": [
|
||||
{
|
||||
"selector": "@e[name=某实体,scores={计分板=0..93}]"
|
||||
},
|
||||
{
|
||||
"selector": "@e[name=某实体,scores={计分板=1..93}]"
|
||||
},
|
||||
{
|
||||
"selector": "@e[name=某实体,scores={计分板=92..93}]"
|
||||
},
|
||||
{
|
||||
"text": "显示第一段"
|
||||
},
|
||||
{
|
||||
"text": "显示第二段"
|
||||
},
|
||||
{
|
||||
"text": "显示第三段"
|
||||
},
|
||||
{
|
||||
"text": "NaN"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 7.6 MiB After Width: | Height: | Size: 130 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1003 KiB |
Binary file not shown.
@@ -0,0 +1,34 @@
|
||||
# 一个简单的项目实践测试
|
||||
from pathlib import Path
|
||||
from Musicreater import load_plugin_from_module, MusiCreater
|
||||
from Musicreater.plugins import _global_plugin_registry
|
||||
|
||||
load_plugin_from_module("Musicreater.builtin_plugins.midi_read")
|
||||
load_plugin_from_module("Musicreater.builtin_plugins.to_commands")
|
||||
load_plugin_from_module("Musicreater.builtin_plugins.commands_to_structure")
|
||||
|
||||
from Musicreater.builtin_plugins.midi_read import MidiImportConfig
|
||||
from Musicreater.builtin_plugins.commands_to_structure import McstructureExportConfig
|
||||
|
||||
print("当前支持的导入格式:", _global_plugin_registry.supported_input_formats())
|
||||
print("当前支持的导出格式:", _global_plugin_registry.supported_output_formats())
|
||||
|
||||
msct = MusiCreater.import_music(
|
||||
Path("./resources/测试片段.mid"), plugin_config=MidiImportConfig()
|
||||
)
|
||||
|
||||
|
||||
print("全局插件注册表:", _global_plugin_registry)
|
||||
print("插件缓存字典:", msct._plugin_cache)
|
||||
|
||||
|
||||
print(msct.music.music_name)
|
||||
|
||||
print(
|
||||
"大小、音乐总长:",
|
||||
msct.export_music(
|
||||
Path("./output.mcstructure"),
|
||||
plugin_id="music_to_mcstructure_in_delay_plugin",
|
||||
plugin_config=McstructureExportConfig(),
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,42 @@
|
||||
# 一个简单的项目实践测试
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from rich import print
|
||||
|
||||
from Musicreater import load_plugin_from_module, MusiCreater
|
||||
from Musicreater.plugins import _global_plugin_registry
|
||||
from Musicreater._utils import incremental_save_path
|
||||
|
||||
load_plugin_from_module("Musicreater.builtin_plugins.midi_read")
|
||||
load_plugin_from_module("Musicreater.builtin_plugins.music_preview")
|
||||
|
||||
from Musicreater.builtin_plugins.midi_read import MidiImportConfig
|
||||
from Musicreater.builtin_plugins.music_preview import PcmConversionConfig
|
||||
|
||||
print("当前支持的导入格式:", _global_plugin_registry.supported_input_formats())
|
||||
print("当前支持的导出格式:", _global_plugin_registry.supported_output_formats())
|
||||
|
||||
msct = MusiCreater.import_music(
|
||||
Path(input("文件路径:")).resolve(),
|
||||
plugin_config=MidiImportConfig(
|
||||
divide_tracks_by_volume=True,
|
||||
),
|
||||
)
|
||||
|
||||
print("全局插件注册表:", _global_plugin_registry)
|
||||
print("插件缓存字典:", msct._plugin_cache)
|
||||
|
||||
print(
|
||||
msct.export_music(
|
||||
(fn := incremental_save_path("output", suffix=".wav")),
|
||||
plugin_id="music_to_pcm_plugin",
|
||||
plugin_config=PcmConversionConfig(
|
||||
assets_path=Path("./vanilla_assets/wav/").resolve(),
|
||||
synthesis_mode=2,
|
||||
pitch_accuracy_decimals=0,
|
||||
),
|
||||
),
|
||||
"\n文件路径:",
|
||||
fn,
|
||||
)
|
||||
+24
-9
@@ -1,24 +1,39 @@
|
||||
|
||||
# 一个简单的项目实践测试
|
||||
from pathlib import Path
|
||||
from Musicreater import load_plugin_module, MusiCreater
|
||||
from Musicreater import load_plugin_from_module, MusiCreater
|
||||
from Musicreater.plugins import _global_plugin_registry
|
||||
|
||||
load_plugin_module("Musicreater.builtin_plugins.midi_read")
|
||||
load_plugin_from_module("Musicreater.builtin_plugins.midi_read")
|
||||
|
||||
from Musicreater.builtin_plugins.midi_read import MidiImportConfig
|
||||
|
||||
print("当前支持的导入格式:", _global_plugin_registry.supported_input_formats())
|
||||
print("当前支持的导出格式:", _global_plugin_registry.supported_output_formats())
|
||||
|
||||
print(msct:=MusiCreater.import_music(Path("./resources/测试片段.mid")))
|
||||
print(msct := MusiCreater.import_music(Path("./resources/测试片段.mid")))
|
||||
|
||||
print(msct.music)
|
||||
|
||||
|
||||
# 为了让类型检查器满意,以下方法不建议使用,因为这本质上是越过了 MusiCreater 类而直接执行插件的函数
|
||||
print(t := msct.midi_2_music_plugin.load(Path("./resources/测试片段.mid"), None))
|
||||
# 如果要直接访问插件里面的函数:
|
||||
# 为了确保类型安全,以下方法不建议使用,因为这本质上是越过了 MusiCreater 类而直接执行插件的函数
|
||||
print(t := msct.midi_to_music_plugin.load(Path("./resources/测试片段.mid"), None)) # type: ignore
|
||||
# 我们建议用这种方式来代替
|
||||
t = _global_plugin_registry._music_input_plugins["midi_2_music_plugin"].load(Path("./resources/测试片段.mid"), None)
|
||||
t = _global_plugin_registry._music_input_plugins["midi_to_music_plugin"].load(
|
||||
Path("./resources/测试片段.mid"),
|
||||
MidiImportConfig(
|
||||
speed_multiplier=1.0,
|
||||
),
|
||||
)
|
||||
# 或者
|
||||
from Musicreater.plugins import MusicInputPluginBase
|
||||
|
||||
if isinstance((p := msct.midi_to_music_plugin), MusicInputPluginBase):
|
||||
t = p.load(Path("./resources/测试片段.mid"), None)
|
||||
|
||||
# 但是说实话,既然已经在 MusiCreater 类中提供了
|
||||
# import_music、export_music、perform_operation_on_music 等方法,
|
||||
# 那么我们不建议使用上面展示的调取插件的方式来执行插件内的函数。
|
||||
msct.perform_operation_on_music
|
||||
|
||||
print(_global_plugin_registry)
|
||||
print(msct._plugin_cache)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user