Compare commits

..

36 Commits

Author SHA1 Message Date
Eilles 62db7e585f 完善新的音量设计,现在我们通过了标准的类型检查 2026-07-23 01:21:00 +08:00
Eilles debd42c633 小重构:音量vs距离新的计算方案 2026-07-22 08:23:16 +08:00
Eilles df3519ebc5 提交一部分新的文档 2026-07-21 00:11:02 +08:00
Eilles 2804760c03 Maybe yes, maybe no, but 音量响度 should let to go 2026-07-20 20:27:29 +08:00
Eilles 813ffab782 需要和重新设计音量的配置方式。开始做实验 2026-07-20 07:14:22 +08:00
EillesWan b01fccbf4b midi读取插件的歌词字符编码问题修复 2026-07-17 04:58:12 +08:00
EillesWan 6b0d88ad5b 和过去的自己斗智斗勇,想知道为什么用球坐标系来表示声像方位吗? 2026-07-17 04:01:17 +08:00
Eilles fc9ec86adb 新增新文件路径小参数,新增音乐预览插件测试样例。修复小问题 2026-07-09 02:06:14 +08:00
Eilles 2c6e831533 修复音轨删除不彻底、预览音量未能调的问题、完整支持小号音色 2026-07-08 06:22:22 +08:00
Eilles 026c529182 完成音乐合成插件之迁移 2026-07-08 02:40:43 +08:00
Eilles 2919722c52 准备迁移 MusicPreview 插件 2026-07-07 02:22:45 +08:00
Eilles 4d780b4922 全曲复制功能 2026-07-04 15:07:31 +08:00
Eilles 7a03511cc9 2026-07-03 18:34:39 +08:00
Eilles 3b6d786346 使精确时间参数实际可用 2026-07-03 18:31:05 +08:00
Eilles 816f3cbc3c 小修改 2026-07-03 03:05:47 +08:00
Eilles 6940a211b4 给部分数据存储内容增设copy和delete函数 2026-07-03 03:04:40 +08:00
Eilles 8cd6865bf4 更新待办事项 2026-05-21 23:44:14 +08:00
EillesWan 825d275a5e yes,这就是 develop 分支更新的样子……比如只更新一些注释 2026-04-27 20:24:49 +08:00
Eilles de830262a7 小修小补 2026-04-07 18:17:29 +08:00
Eilles 9c1383360b Merge branch 'develop' of https://gitee.com/EillesWan/Musicreater into develop 2026-04-04 15:06:17 +08:00
Eilles 60cbdfb9d0 2026-04-04 15:06:14 +08:00
EillesWan d6944392cd 关键函数先上传,为了 音·视 的开发跟上来 2026-04-02 18:21:32 +08:00
Eilles ba7b10a25f 完整使用流程已经测试一遍了,完整流程是没问题的,接下来就是每个节点的那些小功能可能会有一些没有测试到的地方。这些功能虽然细枝末节,但也都举足轻重,应当在开发出了伶伦工作站的时候测试。所以目前的开发重心转移到伶伦工作站上,相关插件从 v2 到 v3 的移植,交由其他人来处理。 2026-03-13 22:56:51 +08:00
EillesWan 307feb9b24 开始准备移植指令转结构的插件 2026-03-13 09:50:23 +08:00
EillesWan 6e518dada4 修复小bug,完成转指令的插件功能。 2026-03-13 09:26:02 +08:00
Eilles 4c036cbd4c 完成进度条动画生成的逻辑,尚未测试。 2026-03-12 23:39:42 +08:00
Eilles c310ba5dc3 Merge branch 'develop' of https://gitee.com/EillesWan/Musicreater into develop 2026-03-04 21:10:57 +08:00
Eilles d9c92fa269 部份依赖没写进dev的extra来,现在好了 2026-03-04 21:10:51 +08:00
EillesWan 00c445f7ad 同步部分内容,切换到学校设备来 2026-03-02 11:58:40 +08:00
EillesWan 3ee686c712 新的故事还在继续,现在提交方便帮忙同时开发 2026-02-23 23:18:19 +08:00
EillesWan 0e95a1e541 把 v2 的转指令功能复制粘贴了上来,稍后再优化,我的灵码还是没有好 2026-02-14 09:21:27 +08:00
EillesWan bbc67921d6 我的通义灵码罢工了,没有内联建议,我也要罢工 2026-02-13 19:19:08 +08:00
EillesWan 62cd4a0c94 优化插件基类、完善插件文档 2026-02-13 02:10:03 +08:00
EillesWan 295da53c60 完善部分文档 2026-02-12 21:57:22 +08:00
EillesWan fff8e43f53 完成 Midi 导入插件移植 2026-02-12 13:24:46 +08:00
EillesWan 2a5ccb8eeb 准备移植 v2 功能到插件来 2026-02-08 06:14:20 +08:00
70 changed files with 11225 additions and 2903 deletions
+12 -2
View File
@@ -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
View File
@@ -1,6 +1,6 @@
# 汉钰律许可协议,第一版
**总第一版 次修订 · 二〇二四年七月七日编 二〇二五年四月二十六日修订**
**总第一版 次修订 · 二〇二四年七月七日编 二〇二六年七月三日修订**
## 一、重要须知
@@ -60,7 +60,7 @@
11. 如果用户对任何民事主体因其在进行本协议所授权进行的活动中侵犯该用户的专利而提起诉讼那么根据本协议授予该用户的所有关于此作品的任何其他专利许可将在提起上述诉讼之日起终止
12. 如果本作品作为用户的其他作品的不可分割的一部分进行任何民事活动本协议依旧对本作品即该用户的其他作品的一部分生效若本作品完全融入该用户的其他作品之中而不可独立存在则该用户需要保证其作品存在与本协议冲突的条款除非该作品已获得此作品颁发者的特殊许可或该用户即为此作品颁发者本人
12. 如果本作品作为用户的其他作品的不可分割的一部分进行任何民事活动本协议依旧对本作品即该用户的其他作品的一部分生效若本作品完全融入该用户的其他作品之中而不可独立存在则该用户需要保证其作品存在与本协议冲突的条款除非该作品已获得此作品颁发者的特殊许可或该用户即为此作品颁发者本人
## 四、使用条件
@@ -90,7 +90,7 @@
## 六、商标相关
本协议并未授予用户将颁发者的商标专属标记或特定产品名称用于合理或惯例性的描述或此类声明之外其他任何位置的权利
本协议不允许用户合理或惯例性的描述或者用于此类声明之外在任何其他位置使用颁发者的商标专用标记或特定产品名称
## 七、免责声明
+9 -5
View File
@@ -5,7 +5,7 @@
是一款免费开源的《我的世界》数字音频支持库。
Musicreater (音·创)
A free and open-source library for handling with **Minecraft** digital music.
A cost free and open-source library for handling with **Minecraft** digital music.
版权所有 © 2026 睿乐组织
Copyright © 2026 TriM-Organization
@@ -27,13 +27,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 +44,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 +66,10 @@ __all__ = [
"SoundAtmos",
"MineNote",
"CurvableParam",
"Instrument",
# 工程项目相关
"load_plugin_module",
"load_plugin_from_module",
"MusiCreater",
# 默认乐器
"DefaultInstrument",
]
+53 -52
View File
@@ -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):
)
# 怎么?
# 插件的彼此依赖就不需要什么调用了吧
# 插件的依赖就不需要什么调用了吧
+108
View File
@@ -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
@@ -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,
)
@@ -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
@@ -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_,
),
)
-59
View File
@@ -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 是 500000tpb 在
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
View File
File diff suppressed because it is too large Load Diff
+723 -155
View File
File diff suppressed because it is too large Load Diff
+20 -5
View File
@@ -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):
"""插件配置相关错误"""
+18 -12
View File
@@ -6,7 +6,7 @@
是一款免费开源的我的世界数字音频支持库
Musicreater (·)
A free and open-source library for handling with **Minecraft** digital music.
A cost free and open-source library for handling with **Minecraft** digital music.
版权所有 © 2026 睿乐组织
Copyright © 2026 TriM-Organization
@@ -41,7 +41,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 +63,7 @@ from .plugins import (
PluginRegistry,
PluginConfig,
PluginTypes,
load_plugin_module,
load_plugin_from_module,
T_IOPlugin,
T_Plugin,
)
@@ -96,9 +96,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 +117,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 +166,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 +180,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 +265,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
View File
@@ -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_keyupdate_key_valueset_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
View File
@@ -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 -5
View File
@@ -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]
"""
拟合函数类型
"""
+62 -27
View File
@@ -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,57 @@
## 介绍 🚀
· 是一款免费开源的针对 **我的世界** 音乐的支持库
· 是一款极致优化的针对 **我的世界** 音乐的支持库通过插件化的设计支持多种处理方式
欢迎加群[861684859](https://jq.qq.com/?_wv=1027&k=hpeRxrYr)
> **注意** 本仓库内的项目仅仅是支持库其用户为基岩版音乐相关软件的开发者
>
> 面向常规用户的 **基岩版音乐转换工具** 请参阅[伶伦转换器](../../../Linglun-Converter)
> 面向常规用户的 **基岩版音乐转换工具** 请参阅[伶伦转换器](../../../Linglun-Converter)已停止更新
>
> 我们也正在开发面向高级用户的 **基岩版音乐编辑工具**数字音频工作站[伶伦](../../../LinglunStudio)
## 安装 🔳
- 使用 pypi
使用 pypi 直接安装
```bash
- ```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 +101,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 +135,7 @@
- 感谢 ****\<QQ237667809\> 反馈在新版本的指令格式下计分板播放器的附加包无法播放的问题
- 感谢 **梦幻duang**\<QQ13753593\> 为我们提供 Java 1.12.2 版本命令格式参考
- 感谢 [_Open Note Block Studio_](https://github.com/OpenNBS/NoteBlockStudio) 项目的开发为我们提供持续的追赶动力
- 感谢 **启航与凡凡**\<QQ2777856500\> 找到 **· v2** 版本音符序列文件解码的错误并指出修正方式
> 感谢广大群友为此库提供的测试和建议等
> 若您对我们有所贡献但您的名字没有出现在此列表中请联系我们
@@ -115,7 +146,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 +162,7 @@
Minecraft Mojang Synergies AB 的商标此项目中所有对于我的世界Minecraft等相关称呼均为必要的介绍性使用
- 上文提及的 网易 公司指代的是在中国大陆运营我的世界中国版的上海网之易璀璨网络科技有限公司
- 上文提及的 网易 公司指代的是在中国大陆运营我的世界中国版的上海网之易吾世界网络科技有限公司
NOT AN OFFICIAL MINECRAFT PRODUCT.
+40 -38
View File
@@ -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,19 +35,19 @@
[简体中文 🇨🇳](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_.
> **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).
> 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_ (digital audio workstation): [Linglun](../../../LinglunStudio)
> We are also developing a _BE music editor_, a new _Minecraft_ digital audio workstation(DAW): [Linglun](../../../LinglunStudio)
## Installation 🔳
@@ -58,17 +58,9 @@ Welcome to join our QQ group: [861684859](https://jq.qq.com/?_wv=1027&k=hpeRxrYr
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 +73,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> &lt;QQ2836146704&gt; 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="&quot;It was once, a convert-QQ-bot is just in front my eyes&quot;&#10;&quot;Until lose, I finally know cannot chase back what I needs&quot;"></td><td><small>&quot;It was once, a convert-QQ-bot is just in front my eyes&quot;<br>&quot;Until lose, I finally know cannot chase back what I needs&quot;</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> &lt;QQ2836146704&gt; 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="&quot;It was once, a convert-QQ-bot is just in front my eyes&quot;&#10;&quot;Until lose, I finally know cannot chase back what I needs&quot;"></td><td><small>&quot;It was once, a convert-QQ-bot is just in front my eyes&quot;<br>&quot;Until lose, I finally know cannot chase back what I needs&quot;</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 +119,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 +133,8 @@ NOT APPROVED BY OR ASSOCIATED WITH NETEASE.
此项目不隶属或关联于 Mojang Studios 微软
此项目亦不隶属或关联于 网易 相关
此项目亦不隶属或关联于 网易
Minecraft Mojang Synergies AB 的商标此项目中所有对于我的世界Minecraft等相关称呼均为必要的介绍性使用
- 上文提及的 网易 公司指代的是在中国大陆运营我的世界中国版的上海网之易璀璨网络科技有限公司
- 上文提及的 网易 公司指代的是在中国大陆运营我的世界中国版的上海网之易吾世界网络科技有限公司
+37 -20
View File
@@ -1,48 +1,65 @@
# 任务清单
## 待办事项
- 乐曲文件格式设计
- - [ ] 乐曲文件格式设计
目前想到的是
1. 使用 `.MCT` 作为项目文件的后缀然后考虑一下格式是否和之前的 MusicSequence 兼容如果兼容的话可以照旧用 `.MSQ`如不的话可以试试想一个新的后缀名作为数据文件后缀
2. 要求数据文件支持完全流式读入
- 音轨静音处理
- - [ ] 音轨静音处理
当前没有处理
- 优化音轨的存储方式
- - [ ] 优化音轨的存储方式
当前是用列表且每一次变动元素都要重新排序这样消耗太大了需要优化改用最小堆形式heapq
- 移植 v2 功能到内置插件
目前 v2 的功能有很多都要移植到 v3
1. 导入 Midi 文件到全曲
2. 导入 Midi 文件到指定轨道
3. 导出到延迟播放器的结构文件MCSTRUCTUREBDX
4. 导出到延迟播放器的附加包
5. 导出到积分板播放器的以上两种形式
6. 导出到中继器播放器的以上两种形式
7. WebSocket 播放器中播放
8. 导出到支持神羽资源包的以上 7 种形式
9. 对于 Midi 歌词的实验性功能
10. 对于 Java 版本适配的实验性功能
11. 对于听感优化的实验性功能插值偏移
1. - [x] 导入 Midi 文件到全曲
2. - [ ] 导入 Midi 文件到指定轨道
3. - [x] 导出到延迟播放器的结构文件MCSTRUCTUREBDX
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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -1,4 +1,4 @@
# 音乐序列文件格式
# 音乐序列文件格式(已过时)
· 库的音符序列文件格式包含两种一种是常规的音乐序列存储采用的 MSQ 格式另一种是为了流式读取音符而采用的 FSQ 格式
+113
View File
@@ -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 方法,会直接用上面展示过的方法输出
+97
View File
@@ -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 方法,会直接用上面展示过的方法读取数据
+20 -20
View File
@@ -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 (
-89
View File
@@ -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)
+28 -24
View File
@@ -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,
)
+95 -41
View File
@@ -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
+7 -230
View File
@@ -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"=",
)
"""
默认的进度条样式
"""
+2 -2
View File
@@ -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
)
+37 -9
View File
@@ -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 = [
@@ -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"
+82
View File
@@ -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)
+32
View File
@@ -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.
+34
View File
@@ -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(),
),
)
+42
View File
@@ -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
View File
@@ -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)
Generated
+4526 -547
View File
File diff suppressed because it is too large Load Diff