From 841f6e53c6863ba5800cf27a06a5d5145c205af5 Mon Sep 17 00:00:00 2001 From: EillesWan Date: Mon, 2 Feb 2026 01:33:47 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=B4=E6=97=B6=E4=B8=8A=E4=BC=A0=EF=BC=8C?= =?UTF-8?q?=E4=BB=8D=E5=9C=A8=E5=BC=80=E5=8F=91=E8=BF=87=E7=A8=8B=E4=B8=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Musicreater/__init__.py | 144 +------ Musicreater/_plugin_abc.py | 412 ++++++++++++++++++ Musicreater/constants.py | 3 + Musicreater/data.py | 156 ++++++- Musicreater/exceptions.py | 8 +- Musicreater/main.py | 80 ++++ Musicreater/old_init.py | 144 +++++++ Musicreater/plugin.py | 783 ---------------------------------- Musicreater/plugins.py | 472 +++++++++++++++++++- Musicreater/types.py | 4 +- Packer/MSCT_Packer.py | 8 +- README.md | 8 +- README_EN.md | 12 +- example.py | 10 +- example_singleConvert.py | 6 +- example_websocket.py | 8 +- let_future_java.py | 2 +- test_fsq_opera.py | 8 +- test_msq_opera.py | 8 +- tests/genexpr_vs_yieldfrom.py | 21 + 20 files changed, 1332 insertions(+), 965 deletions(-) create mode 100644 Musicreater/_plugin_abc.py create mode 100644 Musicreater/main.py create mode 100644 Musicreater/old_init.py delete mode 100644 Musicreater/plugin.py create mode 100644 tests/genexpr_vs_yieldfrom.py diff --git a/Musicreater/__init__.py b/Musicreater/__init__.py index 4f061c3..d8f943a 100644 --- a/Musicreater/__init__.py +++ b/Musicreater/__init__.py @@ -1,19 +1,24 @@ # -*- coding: utf-8 -*- -"""一个简单的我的世界音频转换库 -音·创 (Musicreater) + +""" +音·创 是一款免费开源的《我的世界》数字音频支持库。 -Musicreater(音·创) -A free open source library used for dealing with **Minecraft** digital musics. -版权所有 © 2025 金羿 & 诸葛亮与八卦阵 -Copyright © 2025 Eilles & bgArray +Musicreater (音·创) +A free and open-source library for handling with **Minecraft** digital music. -音·创(“本项目”)的协议颁发者为 金羿、诸葛亮与八卦阵 -The Licensor of Musicreater("this project") is Eilles, bgArray. +版权所有 © 2026 睿乐组织 +Copyright © 2026 TriM-Organization + +音·创(“本项目”)的协议颁发者为 金羿、玉衡Alioth +The Licensor of Musicreater("this project") is Eilles, YuhengAlioth 本项目根据 第一版 汉钰律许可协议(“本协议”)授权。 -任何人皆可从以下地址获得本协议副本:https://gitee.com/EillesWan/YulvLicenses。 -若非因法律要求或经过了特殊准许,此作品在根据本协议“原样”提供的基础上,不予提供任何形式的担保、任何明示、任何暗示或类似承诺。也就是说,用户将自行承担因此作品的质量或性能问题而产生的全部风险。 +任何人皆可从以下地址获得本协议副本: +https://gitee.com/TriM-Organization/Musicreater/blob/master/LICENSE.md。 +若非因法律要求或经过了特殊准许,此作品在根据本协议“原样”提供的基础上, +不予提供任何形式的担保、任何明示、任何暗示或类似承诺。 +也就是说,用户将自行承担因此作品的质量或性能问题而产生的全部风险。 详细的准许和限制条款请见原协议文本。 """ @@ -22,123 +27,12 @@ The Licensor of Musicreater("this project") is Eilles, bgArray. # 若需转载或借鉴 许可声明请查看仓库目录下的 License.md -__version__ = "2.4.2.3" -__vername__ = "音符附加信息升级" + +__version__ = "3.0.0-alpha" + __author__ = ( ("金羿", "Eilles"), - ("诸葛亮与八卦阵", "bgArray"), + ("玉衡Alioth", "YuhengAlioth"), ("鱼旧梦", "ElapsingDreams"), ("偷吃不是Touch", "Touch"), ) -__all__ = [ - # 主要类 - "MusicSequence", - "MidiConvert", - # 附加类 - # "SingleNote", - "MineNote", - "MineCommand", - "SingleNoteBox", - "ProgressBarStyle", - # "TimeStamp", 未来功能 - # 字典键 - "MIDI_PROGRAM", - "MIDI_VOLUME", - "MIDI_PAN", - # 默认值 - "MIDI_DEFAULT_PROGRAM_VALUE", - "MIDI_DEFAULT_VOLUME_VALUE", - "DEFAULT_PROGRESSBAR_STYLE", - # Midi 自己的对照表 - "MIDI_PITCH_NAME_TABLE", - "MIDI_PITCHED_NOTE_NAME_GROUP", - "MIDI_PITCHED_NOTE_NAME_TABLE", - "MIDI_PERCUSSION_NOTE_NAME_TABLE", - # Minecraft 自己的对照表 - "MC_PERCUSSION_INSTRUMENT_LIST", - "MC_PITCHED_INSTRUMENT_LIST", - "MC_INSTRUMENT_BLOCKS_TABLE", - "MC_EILLES_RTJE12_INSTRUMENT_REPLACE_TABLE", - "MC_EILLES_RTBETA_INSTRUMENT_REPLACE_TABLE", - # 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", - # 操作性函数 - "velocity_2_distance_natural", - "velocity_2_distance_straight", - "panning_2_rotation_linear", - "panning_2_rotation_trigonometric", - # 工具函数 - "load_decode_musicsequence_metainfo", - "load_decode_msq_flush_release", - "load_decode_fsq_flush_release", - "guess_deviation", - "mctick2timestr", - "midi_inst_to_mc_sound", -] - -from .old_main import MusicSequence, MidiConvert - -from .subclass import ( - MineNote, - MineCommand, - SingleNoteBox, - ProgressBarStyle, - mctick2timestr, - DEFAULT_PROGRESSBAR_STYLE, -) - -from .utils import ( - # 兼容性函数 - load_decode_musicsequence_metainfo, - load_decode_msq_flush_release, - load_decode_fsq_flush_release, - # 工具函数 - 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, - MIDI_DEFAULT_VOLUME_VALUE, - # MIDI 表 - MIDI_PITCH_NAME_TABLE, - MIDI_PITCHED_NOTE_NAME_GROUP, - MIDI_PITCHED_NOTE_NAME_TABLE, - MIDI_PERCUSSION_NOTE_NAME_TABLE, - # 我的世界 表 - MC_EILLES_RTBETA_INSTRUMENT_REPLACE_TABLE, - MC_EILLES_RTJE12_INSTRUMENT_REPLACE_TABLE, - MC_INSTRUMENT_BLOCKS_TABLE, - MC_PERCUSSION_INSTRUMENT_LIST, - MC_PITCHED_INSTRUMENT_LIST, - # 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, -) diff --git a/Musicreater/_plugin_abc.py b/Musicreater/_plugin_abc.py new file mode 100644 index 0000000..a6fd87d --- /dev/null +++ b/Musicreater/_plugin_abc.py @@ -0,0 +1,412 @@ +# -*- coding: utf-8 -*- + +""" +存储 音·创 v3 的插件基类,提供抽象接口以供实际插件使用 +""" + +""" +版权所有 © 2025 金羿 +Copyright © 2025 Eilles + +开源相关声明请见 仓库根目录下的 License.md +Terms & Conditions: License.md in the root directory +""" + +# 睿乐组织 开发交流群 861684859 +# Email TriM-Organization@hotmail.com +# 若需转载或借鉴 许可声明请查看仓库目录下的 License.md + +# ===================== +# NOTE: [WARNING] +# 这个文件是一坨屎山代码 +# 请勿模仿,请多包容 +# ===================== + + +import sys + +from abc import ABC, abstractmethod +from dataclasses import dataclass +from enum import Enum +from pathlib import Path +from typing import ( + Dict, + Any, + Optional, + List, + Tuple, + Union, + Sequence, + BinaryIO, + Generator, + Iterator, + Set, +) + +if sys.version_info >= (3, 11): + import tomllib + import tomli_w +else: + import tomli as tomllib # 第三方包 + import tomli_w + +from .exceptions import ( + PluginConfigDumpError, + PluginConfigLoadError, + PluginMetainfoNotFoundError, + PluginMetainfoTypeError, + PluginMetainfoValueError, + PluginAttributeNotFoundError, + ParameterTypeError, + PluginInstanceNotFoundError, +) +from .data import SingleMusic, SingleTrack + +# 已经全部由 plugins.py 提供接口 +# 请用户从 plugins.py 导入 +# 不要在这里导,会坏掉的 + +# __all__ = [ +# # 枚举类 +# "PluginType", +# # 抽象基类/数据类(插件参数定义) +# "PluginConfig", +# "PluginMetaInformation", +# # 抽象基类(插件定义) +# "MusicInputPlugin", +# "TrackInputPlugin", +# "MusicOperatePlugin", +# "TrackOperatePlugin", +# "MusicOutputPlugin", +# "TrackOutputPlugin", +# "ServicePlugin", +# "LibraryPlugin", +# # 插件注册用装饰函数 +# "music_input_plugin", +# "track_input_plugin", +# "music_operate_plugin", +# "track_operate_plugin", +# "music_output_plugin", +# "track_output_plugin", +# "service_plugin", +# "library_plugin", +# ] + + +@dataclass +class PluginConfig(ABC): + """插件配置基类""" + + def to_dict(self) -> Dict[str, Any]: + """字典化配置文件""" + return {k: v for k, v in self.__dict__.items() if not k.startswith("_")} + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "PluginConfig": + """从字典创建配置实例""" + # 只保留类中定义的字段 + field_names = {f.name for f in cls.__dataclass_fields__.values()} + filtered_data = {k: v for k, v in data.items() if k in field_names} + return cls(**filtered_data) + + def save_to_file(self, file_path: Path) -> None: + """保存配置到文件""" + if file_path.suffix.upper() == ".TOML": + file_path.parent.mkdir(parents=True, exist_ok=True) + else: + raise PluginConfigDumpError( + "插件配置文件类型不应为`{}`,须为`TOML`格式。".format(file_path.suffix) + ) + + try: + 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) + + @classmethod + def load_from_file(cls, file_path: Path) -> "PluginConfig": + """从文件加载配置""" + try: + with file_path.open("rb") as f: + return cls.from_dict(tomllib.load(f)) + except Exception as e: + raise PluginConfigLoadError(e) + + +class PluginType(str, Enum): + """插件类型枚举""" + + FUNCTION_IMPORT = "import_data" + FUNCTION_EXPORT = "export_data" + FUNCTION_OPERATE = "data_operate" + SERVICE = "service" + LIBRARY = "library" + + +@dataclass +class PluginMetaInformation(ABC): + """插件元信息""" + + name: str + """插件名称,应为惟一之名""" + author: str + """插件作者""" + description: str + """插件简介""" + version: Tuple[int, ...] + """插件版本号""" + type: PluginType + """插件类型""" + license: str = "MIT License" + """插件发布时采用的许可协议""" + dependencies: Sequence[str] = [] + """插件是否对其他插件存在依赖""" + + +class TopBasePlugin(ABC): + """所有插件的抽象基类""" + + metainfo: PluginMetaInformation + """插件元信息""" + + def __init_subclass__(cls) -> None: + super().__init_subclass__() + if hasattr(cls, "metainfo"): + if not isinstance(cls.metainfo, PluginMetaInformation): + raise PluginMetainfoTypeError( + "类`{cls_name}`之属性`metainfo`的类型,必须为`PluginMetaInformation`".format( + cls_name=cls.__name__ + ) + ) + else: + raise PluginMetainfoNotFoundError( + "类`{cls_name}`必须定义一个`metainfo`属性。".format( + cls_name=cls.__name__ + ) + ) + + +class TopInOutBasePlugin(TopBasePlugin, ABC): + """导入导出用抽象基类""" + + supported_formats: Tuple[str, ...] = tuple() + """支持的格式""" + + def __init_subclass__(cls) -> None: + super().__init_subclass__() + + if hasattr(cls, "supported_formats"): + if cls.supported_formats: + # 强制转换为大写,并使用元组 + cls.supported_formats = tuple(map(str.upper, cls.supported_formats)) + else: + cls.supported_formats = tuple() + else: + raise PluginAttributeNotFoundError( + "用于导入导出数据的类`{cls_name}`必须定义一个`supported_formats`属性。".format( + cls_name=cls.__name__ + ) + ) + + def can_handle_file(self, file_path: Path) -> bool: + """判断是否可处理某个文件""" + return file_path.suffix.upper().endswith(self.supported_formats) + + def can_handle_format(self, format_name: str) -> bool: + """判断是否可处理某个格式""" + return format_name.upper().endswith(self.supported_formats) + + +class MusicInputPlugin(TopInOutBasePlugin, ABC): + """导入用插件抽象基类-完整曲目""" + + def __init_subclass__(cls) -> None: + super().__init_subclass__() + + if cls.metainfo.type != PluginType.FUNCTION_IMPORT: + raise PluginMetainfoValueError( + "插件类`{cls_name}`是从`MusicInputPlugin`继承的,该类的子类应当为一个`PluginType.FUNCTION_IMPORT`类型的插件,而不是`PluginType.{cls_type}`".format( + cls_name=cls.__name__, + cls_type=cls.metainfo.type.name, + ) + ) + + @abstractmethod + def loadbytes( + self, bytes_buffer_in: BinaryIO, config: Optional[PluginConfig] + ) -> "SingleMusic": + """从字节流加载数据到完整曲目""" + pass + + def load(self, file_path: Path, config: Optional[PluginConfig]) -> "SingleMusic": + """从文件加载数据到完整曲目""" + with file_path.open("rb") as f: + return self.loadbytes(f, config) + + +class TrackInputPlugin(TopInOutBasePlugin, ABC): + """导入用插件抽象基类-单个音轨""" + + def __init_subclass__(cls) -> None: + super().__init_subclass__() + + if cls.metainfo.type != PluginType.FUNCTION_IMPORT: + raise PluginMetainfoValueError( + "插件类`{cls_name}`是从`TrackInputPlugin`继承的,该类的子类应当为一个`PluginType.FUNCTION_IMPORT`类型的插件,而不是`PluginType.{cls_type}`".format( + cls_name=cls.__name__, + cls_type=cls.metainfo.type.name, + ) + ) + + @abstractmethod + def loadbytes( + self, bytes_buffer_in: BinaryIO, config: Optional[PluginConfig] + ) -> "SingleTrack": + """从字节流加载音符数据到单个音轨""" + pass + + def load(self, file_path: Path, config: Optional[PluginConfig]) -> "SingleTrack": + """从文件加载音符数据到单个音轨""" + with file_path.open("rb") as f: + return self.loadbytes(f, config) + + +class MusicOperatePlugin(TopBasePlugin, ABC): + """音乐处理用插件抽象基类-完整曲目""" + + def __init_subclass__(cls) -> None: + super().__init_subclass__() + + if cls.metainfo.type != PluginType.FUNCTION_OPERATE: + raise PluginMetainfoValueError( + "插件类`{cls_name}`是从`MusicOperatePlugin`继承的,该类的子类应当为一个`PluginType.FUNCTION_OPERATE`类型的插件,而不是`PluginType.{cls_type}`".format( + cls_name=cls.__name__, + cls_type=cls.metainfo.type.name, + ) + ) + + @abstractmethod + def process( + self, data: "SingleMusic", config: Optional[PluginConfig] + ) -> "SingleMusic": + """处理完整曲目的数据""" + pass + + +class TrackOperatePlugin(TopBasePlugin, ABC): + """音乐处理用插件抽象基类-单个音轨""" + + def __init_subclass__(cls) -> None: + super().__init_subclass__() + + if cls.metainfo.type != PluginType.FUNCTION_OPERATE: + raise PluginMetainfoValueError( + "插件类`{cls_name}`是从`TrackOperatePlugin`继承的,该类的子类应当为一个`PluginType.FUNCTION_OPERATE`类型的插件,而不是`PluginType.{cls_type}`".format( + cls_name=cls.__name__, + cls_type=cls.metainfo.type.name, + ) + ) + + @abstractmethod + def process( + self, data: "SingleTrack", config: Optional[PluginConfig] + ) -> "SingleTrack": + """处理单个音轨的音符数据""" + pass + + +class MusicOutputPlugin(TopInOutBasePlugin, ABC): + """导出用插件的抽象基类-完整曲目""" + + def __init_subclass__(cls) -> None: + super().__init_subclass__() + + if cls.metainfo.type != PluginType.FUNCTION_EXPORT: + raise PluginMetainfoValueError( + "插件类`{cls_name}`是从`MusicOutputPlugin`继承的,该类的子类应当为一个`PluginType.FUNCTION_EXPORT`类型的插件,而不是`PluginType.{cls_type}`".format( + cls_name=cls.__name__, + cls_type=cls.metainfo.type.name, + ) + ) + + @abstractmethod + def dumpbytes( + self, data: "SingleMusic", config: Optional[PluginConfig] + ) -> BinaryIO: + """将完整曲目导出为对应格式的字节流""" + pass + + @abstractmethod + def dump( + self, data: "SingleMusic", file_path: Path, config: Optional[PluginConfig] + ): + """将完整曲目导出为对应格式的文件""" + pass + + +class TrackOutputPlugin(TopInOutBasePlugin, ABC): + """导出用插件的抽象基类-单个音轨""" + + def __init_subclass__(cls) -> None: + super().__init_subclass__() + + if cls.metainfo.type != PluginType.FUNCTION_EXPORT: + raise PluginMetainfoValueError( + "插件类`{cls_name}`是从`TrackOutputPlugin`继承的,该类的子类应当为一个`PluginType.FUNCTION_EXPORT`类型的插件,而不是`PluginType.{cls_type}`".format( + cls_name=cls.__name__, + cls_type=cls.metainfo.type.name, + ) + ) + + @abstractmethod + def dumpbytes( + self, data: "SingleTrack", config: Optional[PluginConfig] + ) -> BinaryIO: + """将单个音轨导出为对应格式的字节流""" + pass + + @abstractmethod + def dump( + self, data: "SingleTrack", file_path: Path, config: Optional[PluginConfig] + ): + """将单个音轨导出为对应格式的文件""" + pass + + +class ServicePlugin(TopBasePlugin, ABC): + """服务插件抽象基类""" + + def __init_subclass__(cls) -> None: + super().__init_subclass__() + + if cls.metainfo.type != PluginType.SERVICE: + raise PluginMetainfoValueError( + "插件类`{cls_name}`是从`ServicePlugin`继承的,该类的子类应当为一个`PluginType.SERVICE`类型的插件,而不是`PluginType.{cls_type}`".format( + cls_name=cls.__name__, + cls_type=cls.metainfo.type.name, + ) + ) + + @abstractmethod + def serve(self, config: Optional[PluginConfig], *args) -> None: + """服务插件的运行逻辑""" + pass + + +class LibraryPlugin(TopBasePlugin, ABC): + """插件依赖库的抽象基类""" + + def __init_subclass__(cls) -> None: + super().__init_subclass__() + + if cls.metainfo.type != PluginType.LIBRARY: + raise PluginMetainfoValueError( + "插件类`{cls_name}`是从`LibraryPlugin`继承的,该类的子类应当为一个`PluginType.LIBRARY`类型的插件,而不是`PluginType.{cls_type}`".format( + cls_name=cls.__name__, + cls_type=cls.metainfo.type.name, + ) + ) + + # 怎么? + # 插件的彼此依赖就不需要什么调用了吧 diff --git a/Musicreater/constants.py b/Musicreater/constants.py index dc202ac..c2f3848 100644 --- a/Musicreater/constants.py +++ b/Musicreater/constants.py @@ -44,6 +44,9 @@ MIDI_PAN = "pan" """Midi通道立体声场偏移""" + + + # Midi用对照表 MIDI_DEFAULT_VOLUME_VALUE: int = ( diff --git a/Musicreater/data.py b/Musicreater/data.py index f30d77d..fd6a3e6 100644 --- a/Musicreater/data.py +++ b/Musicreater/data.py @@ -16,7 +16,13 @@ Terms & Conditions: License.md in the root directory # Email TriM-Organization@hotmail.com # 若需转载或借鉴 许可声明请查看仓库目录下的 License.md +# “ +# 把代码 洒落在这里 +# 和音符 留下的沙砾 +# 一点一点爬进你类定义的缝隙 +# ” —— 乐曲访问 by resnah +import heapq from math import sin, cos, asin, radians, degrees, sqrt, atan, inf, ceil from dataclasses import dataclass from typing import ( @@ -33,12 +39,16 @@ from typing import ( Iterable, Iterator, Literal, + Hashable, + TypeVar, ) from enum import Enum -from .exceptions import SingleNoteDecodeError, ParameterTypeError +from .exceptions import SingleNoteDecodeError, ParameterTypeError, ParameterValueError from .paramcurve import ParamCurve +T = TypeVar("T") + class SoundAtmos: """声源方位类""" @@ -411,6 +421,9 @@ class SingleTrack(List[SingleNote]): track_name: str """轨道之名称""" + is_enabled: bool + """该音轨是否启用""" + track_instrument: str """乐器ID""" @@ -440,12 +453,16 @@ class SingleTrack(List[SingleNote]): precise_time: bool = True, percussion: bool = False, sound_direction: SoundAtmos = SoundAtmos(), + enabled: bool = True, extra_information: Dict[str, Any] = {}, *args: SingleNote, ): self.track_name = name """音轨名称""" + self.is_enabled = enabled + """音轨启用情况""" + self.track_instrument = instrument """乐器ID""" @@ -494,27 +511,40 @@ class SingleTrack(List[SingleNote]): ) ) super().append(item) - super().sort() + super().sort() # =========================== TODO 需要优化 def update(self, items: Iterable[SingleNote]): """ 拼接两个音轨 """ super().extend(items) - super().sort() + super().sort() # =========================== TODO 需要优化 - def get(self, time: int) -> Iterator[SingleNote]: + def get(self, time: int) -> Generator[SingleNote, None, None]: """通过开始时间来获取音符""" - return filter(lambda x: x.start_time == time, self) + return (x for x in self if x.start_time == time) - def get_range( + def get_notes( self, start_time: float, end_time: float = inf - ) -> Iterator[SingleNote]: + ) -> Generator[SingleNote, None, None]: """通过开始时间和结束时间来获取音符""" - - return filter( - lambda x: (x.start_time >= start_time) and (x.start_time <= end_time), self + if end_time < start_time: + raise ParameterValueError( + "获取音符的时间范围有误,终止时间`{}`早于起始时间`{}`".format( + end_time, start_time + ) + ) + elif start_time < 0 or end_time < 0: + raise ParameterValueError( + "获取音符的时间范围有误,终止时间`{}`和起始时间`{}`皆不可为负数".format( + end_time, start_time + ) + ) + return ( + x + for x in self + if (x.start_time >= start_time) and (x.start_time <= end_time) ) def get_minenotes( @@ -522,7 +552,7 @@ class SingleTrack(List[SingleNote]): ) -> Generator[MineNote, Any, None]: """获取能够用以在我的世界播放的音符数据类""" - for _note in self.get_range(range_start_time, range_end_time): + for _note in self.get_notes(range_start_time, range_end_time): yield MineNote.from_single_note( note=_note, note_instrument=self.track_instrument, @@ -626,9 +656,107 @@ class SingleMusic(List[SingleTrack]): return len(self) @property - def music_tracks(self) -> List[SingleTrack]: - """音轨列表""" - return self + def music_tracks(self) -> Iterator[SingleTrack]: + """音轨列表,不包含被禁用的音轨""" + return (track for track in self if track.is_enabled) + + @staticmethod + def yield_from_tracks( + tracks: Sequence[Iterator[T]], + sort_key: Callable[[T], Any], + is_subseq_sorted: bool = True, + ) -> Iterator[T]: + """从任意迭代器列表迭代符合顺序的元素 + (惰性多路归并多个迭代器,按 sort_key 排序) + + 参数 + ---- + tracks: Sequence[Iterator[T]] + 迭代器列表 + sort_key: Callable[[T], Any] + 接受 T 元素,返回可比较的键 + is_subseq_sorted: bool = True + 子序列是否已排序 + + 迭代 + ---- + 归并后的每个元素,按 sort_key 升序 + """ + if is_subseq_sorted: + return heapq.merge(*tracks, key=sort_key) + else: + # 初始化堆 + heap_pool: List[Tuple[Any, int, T]] = [] + for _index, _track in enumerate(tracks): + try: + item = next(_track) + heapq.heappush(heap_pool, (sort_key(item), _index, item)) + except StopIteration: + continue + + # 归并主循环 + while heap_pool: + _key, _index, item = heapq.heappop(heap_pool) + yield item + try: + next_item = next(tracks[_index]) + heapq.heappush(heap_pool, (sort_key(next_item), _index, next_item)) + except StopIteration: + pass + # NEVER REACH: + # pool: List[Tuple[str, T]] = [] + # remove_track: List[str] = [] + # for _name, _track in tracks.items(): + # try: + # pool.append((_name, next(_track))) + # except StopIteration: + # remove_track.append(_name) + # for _x in remove_track: + # tracks.pop(_x) + # del remove_track + # while tracks and pool: + # yield (_x := min(pool, key=sort_key))[1] + # try: + # pool.append((_x[0], next(tracks[_x[0]]))) + # except StopIteration: + # tracks.pop(_x[0]) + # pool.sort(key=sort_key) + # for _remain in pool: + # yield _remain[1] + + def get_tracked_notes( + self, start_time: float, end_time: float = inf + ) -> Generator[Iterator[SingleNote], Any, None]: + """获取指定时间段的各个音轨的音符数据""" + return (track.get_notes(start_time, end_time) for track in self) + + def get_tracked_minenotes( + self, start_time: float, end_time: float = inf + ) -> Generator[Iterator[MineNote], Any, None]: + """获取指定时间段的各个音轨的,供我的世界播放的音符数据类""" + return (track.get_minenotes(start_time, end_time) for track in self) + + def get_notes( + self, start_time: float, end_time: float = inf + ) -> Iterator[SingleNote]: + """获取指定时间段的所有音符数据,按照时间顺序""" + if self.track_amount == 0: + return iter(()) + return self.yield_from_tracks( + [track.get_notes(start_time, end_time) for track in self], + sort_key=lambda x: x.start_time, + ) + + def get_minenotes( + self, start_time: float, end_time: float = inf + ) -> Generator[MineNote, Any, None]: + """获取指定时间段所有的,供我的世界播放的音符数据类,按照时间顺序""" + if self.track_amount == 0: + return + yield from self.yield_from_tracks( + [track.get_minenotes(start_time, end_time) for track in self], + sort_key=lambda x: x.start_tick, + ) def set_info(self, key: Union[str, Sequence[str]], value: Any): """设置附加信息""" diff --git a/Musicreater/exceptions.py b/Musicreater/exceptions.py index a9dd67a..037a894 100644 --- a/Musicreater/exceptions.py +++ b/Musicreater/exceptions.py @@ -5,8 +5,8 @@ """ """ -版权所有 © 2025 金羿 & 玉衡 -Copyright © 2025 Eilles & Alioth +版权所有 © 2025 金羿 & 玉衡Alioth +Copyright © 2025 Eilles & YuhengAlioth 开源相关声明请见 仓库根目录下的 License.md Terms & Conditions: License.md in the root directory @@ -16,6 +16,10 @@ Terms & Conditions: License.md in the root directory # Email TriM-Organization@hotmail.com # 若需转载或借鉴 许可声明请查看仓库目录下的 License.md +# “ +# There are planty of "exception"s in this library +# for I know I will always go with my heart. +# ” —— Cyberdevil by resnah class MusicreaterBaseException(Exception): diff --git a/Musicreater/main.py b/Musicreater/main.py new file mode 100644 index 0000000..b8e08af --- /dev/null +++ b/Musicreater/main.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- + + +""" +音·创 +是一款免费开源的《我的世界》数字音频支持库。 + +Musicreater (音·创) +A free and open-source library for handling with **Minecraft** digital music. + +版权所有 © 2026 睿乐组织 +Copyright © 2026 TriM-Organization + +音·创(“本项目”)的协议颁发者为 金羿、玉衡Alioth +The Licensor of Musicreater("this project") is Eilles, YuhengAlioth + +本项目根据 第一版 汉钰律许可协议(“本协议”)授权。 +任何人皆可从以下地址获得本协议副本: +https://gitee.com/TriM-Organization/Musicreater/blob/master/LICENSE.md。 +若非因法律要求或经过了特殊准许,此作品在根据本协议“原样”提供的基础上, +不予提供任何形式的担保、任何明示、任何暗示或类似承诺。 +也就是说,用户将自行承担因此作品的质量或性能问题而产生的全部风险。 +详细的准许和限制条款请见原协议文本。 +""" + +# 音·创 开发交流群 861684859 +# Email TriM-Organization@hotmail.com +# 若需转载或借鉴 许可声明请查看仓库根目录下的 License.md + + +# BUG退散!BUG退散! +# 异常与错误作乱之时 +# 二六字组!万国码合!二六字组!万国码合! +# 赶快呼叫 程序员!Let's Go! + +# BUG退散!BUG退散! +# 異常、誤りが、困った時は +# パラメータ メソッド!パラメータ メソッド! +# 助けてもらおう、開発者!レッツゴー! + +# Bug retreat! Bug retreat! +# Exceptions and errors are causing chaos +# Words combine! Codes unite! +# Hurry to call the programmer! Let's Go! + + +from typing import Dict, Generator, List, Optional, Tuple, Union +from pathlib import Path + +from .data import SingleMusic, SingleTrack +from ._plugin_abc import TopBasePlugin +from .plugins import __global_plugin_registry, PluginRegistry + + +class MusiCreater: + """ + 音·创 v3 主要控制类 + 另:“创建者”一词的英文应该是“Creator” + """ + + __plugin_registry: PluginRegistry + """插件注册表实例""" + _plugin_cache: Dict[str, TopBasePlugin] + """插件缓存字典,插件名为键、插件实例为值""" + music: SingleMusic + """当前曲目实例""" + + def __init__(self, whole_music: SingleMusic) -> None: + global __global_plugin_registry + + self.__plugin_registry = __global_plugin_registry + + self._plugin_cache = {} + + self.music = whole_music + + + + + def import_music(self, file_path: Path, plugin_name: Optional[str] = None) -> SingleMusic: \ No newline at end of file diff --git a/Musicreater/old_init.py b/Musicreater/old_init.py new file mode 100644 index 0000000..4f061c3 --- /dev/null +++ b/Musicreater/old_init.py @@ -0,0 +1,144 @@ +# -*- coding: utf-8 -*- +"""一个简单的我的世界音频转换库 +音·创 (Musicreater) +是一款免费开源的《我的世界》数字音频支持库。 +Musicreater(音·创) +A free open source library used for dealing with **Minecraft** digital musics. + +版权所有 © 2025 金羿 & 诸葛亮与八卦阵 +Copyright © 2025 Eilles & bgArray + +音·创(“本项目”)的协议颁发者为 金羿、诸葛亮与八卦阵 +The Licensor of Musicreater("this project") is Eilles, bgArray. + +本项目根据 第一版 汉钰律许可协议(“本协议”)授权。 +任何人皆可从以下地址获得本协议副本:https://gitee.com/EillesWan/YulvLicenses。 +若非因法律要求或经过了特殊准许,此作品在根据本协议“原样”提供的基础上,不予提供任何形式的担保、任何明示、任何暗示或类似承诺。也就是说,用户将自行承担因此作品的质量或性能问题而产生的全部风险。 +详细的准许和限制条款请见原协议文本。 +""" + +# 睿乐组织 开发交流群 861684859 +# Email TriM-Organization@hotmail.com +# 若需转载或借鉴 许可声明请查看仓库目录下的 License.md + + +__version__ = "2.4.2.3" +__vername__ = "音符附加信息升级" +__author__ = ( + ("金羿", "Eilles"), + ("诸葛亮与八卦阵", "bgArray"), + ("鱼旧梦", "ElapsingDreams"), + ("偷吃不是Touch", "Touch"), +) +__all__ = [ + # 主要类 + "MusicSequence", + "MidiConvert", + # 附加类 + # "SingleNote", + "MineNote", + "MineCommand", + "SingleNoteBox", + "ProgressBarStyle", + # "TimeStamp", 未来功能 + # 字典键 + "MIDI_PROGRAM", + "MIDI_VOLUME", + "MIDI_PAN", + # 默认值 + "MIDI_DEFAULT_PROGRAM_VALUE", + "MIDI_DEFAULT_VOLUME_VALUE", + "DEFAULT_PROGRESSBAR_STYLE", + # Midi 自己的对照表 + "MIDI_PITCH_NAME_TABLE", + "MIDI_PITCHED_NOTE_NAME_GROUP", + "MIDI_PITCHED_NOTE_NAME_TABLE", + "MIDI_PERCUSSION_NOTE_NAME_TABLE", + # Minecraft 自己的对照表 + "MC_PERCUSSION_INSTRUMENT_LIST", + "MC_PITCHED_INSTRUMENT_LIST", + "MC_INSTRUMENT_BLOCKS_TABLE", + "MC_EILLES_RTJE12_INSTRUMENT_REPLACE_TABLE", + "MC_EILLES_RTBETA_INSTRUMENT_REPLACE_TABLE", + # 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", + # 操作性函数 + "velocity_2_distance_natural", + "velocity_2_distance_straight", + "panning_2_rotation_linear", + "panning_2_rotation_trigonometric", + # 工具函数 + "load_decode_musicsequence_metainfo", + "load_decode_msq_flush_release", + "load_decode_fsq_flush_release", + "guess_deviation", + "mctick2timestr", + "midi_inst_to_mc_sound", +] + +from .old_main import MusicSequence, MidiConvert + +from .subclass import ( + MineNote, + MineCommand, + SingleNoteBox, + ProgressBarStyle, + mctick2timestr, + DEFAULT_PROGRESSBAR_STYLE, +) + +from .utils import ( + # 兼容性函数 + load_decode_musicsequence_metainfo, + load_decode_msq_flush_release, + load_decode_fsq_flush_release, + # 工具函数 + 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, + MIDI_DEFAULT_VOLUME_VALUE, + # MIDI 表 + MIDI_PITCH_NAME_TABLE, + MIDI_PITCHED_NOTE_NAME_GROUP, + MIDI_PITCHED_NOTE_NAME_TABLE, + MIDI_PERCUSSION_NOTE_NAME_TABLE, + # 我的世界 表 + MC_EILLES_RTBETA_INSTRUMENT_REPLACE_TABLE, + MC_EILLES_RTJE12_INSTRUMENT_REPLACE_TABLE, + MC_INSTRUMENT_BLOCKS_TABLE, + MC_PERCUSSION_INSTRUMENT_LIST, + MC_PITCHED_INSTRUMENT_LIST, + # 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, +) diff --git a/Musicreater/plugin.py b/Musicreater/plugin.py deleted file mode 100644 index 27b07b0..0000000 --- a/Musicreater/plugin.py +++ /dev/null @@ -1,783 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -存储 音·创 v3 的插件接口与管理相关,提供抽象基类以供其他插件使用 -""" - -""" -版权所有 © 2025 金羿 -Copyright © 2025 Eilles - -开源相关声明请见 仓库根目录下的 License.md -Terms & Conditions: License.md in the root directory -""" - -# 睿乐组织 开发交流群 861684859 -# Email TriM-Organization@hotmail.com -# 若需转载或借鉴 许可声明请查看仓库目录下的 License.md - -# ===================== -# NOTE: [WARNING] -# 这个文件是一坨屎山代码 -# 请勿模仿,请多包容 -# ===================== - - -import sys - -from abc import ABC, abstractmethod, ABCMeta -from dataclasses import dataclass -from enum import Enum -from pathlib import Path -from typing import ( - Dict, - Any, - Optional, - List, - Tuple, - Union, - Sequence, - BinaryIO, - Generator, - Iterator, - Set, -) -from itertools import chain - -if sys.version_info >= (3, 11): - import tomllib - import tomli_w -else: - import tomli as tomllib # 第三方包 - import tomli_w - -from .exceptions import ( - PluginConfigDumpError, - PluginConfigLoadError, - PluginMetainfoNotFoundError, - PluginMetainfoTypeError, - PluginMetainfoValueError, - PluginAttributeNotFoundError, - ParameterTypeError, - PluginInstanceNotFoundError, -) -from .data import SingleMusic, SingleTrack - -__all__ = [ - # 枚举类 - "PluginType", - # 抽象基类/数据类(插件参数定义) - "PluginConfig", - "PluginMetaInformation", - # 抽象基类(插件定义) - "MusicInputPlugin", - "TrackInputPlugin", - "MusicOperatePlugin", - "TrackOperatePlugin", - "MusicOutputPlugin", - "TrackOutputPlugin", - "ServicePlugin", - "LibraryPlugin", - # 插件注册用装饰函数 - "music_input_plugin", - "track_input_plugin", - "music_operate_plugin", - "track_operate_plugin", - "music_output_plugin", - "track_output_plugin", - "service_plugin", - "library_plugin", - # 全局插件注册表 - "plugin_registry", -] - - -@dataclass -class PluginConfig(ABC): - """插件配置基类""" - - def to_dict(self) -> Dict[str, Any]: - """字典化配置文件""" - return {k: v for k, v in self.__dict__.items() if not k.startswith("_")} - - @classmethod - def from_dict(cls, data: Dict[str, Any]) -> "PluginConfig": - """从字典创建配置实例""" - # 只保留类中定义的字段 - field_names = {f.name for f in cls.__dataclass_fields__.values()} - filtered_data = {k: v for k, v in data.items() if k in field_names} - return cls(**filtered_data) - - def save_to_file(self, file_path: Path) -> None: - """保存配置到文件""" - if file_path.suffix.upper() == ".TOML": - file_path.parent.mkdir(parents=True, exist_ok=True) - else: - raise PluginConfigDumpError( - "插件配置文件类型不应为`{}`,须为`TOML`格式。".format(file_path.suffix) - ) - - try: - 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) - - @classmethod - def load_from_file(cls, file_path: Path) -> "PluginConfig": - """从文件加载配置""" - try: - with file_path.open("rb") as f: - return cls.from_dict(tomllib.load(f)) - except Exception as e: - raise PluginConfigLoadError(e) - - -class PluginType(str, Enum): - """插件类型枚举""" - - FUNCTION_IMPORT = "import_data" - FUNCTION_EXPORT = "export_data" - FUNCTION_OPERATE = "data_operate" - SERVICE = "service" - LIBRARY = "library" - - -@dataclass -class PluginMetaInformation(ABC): - """插件元信息""" - - name: str - """插件名称,应为惟一之名""" - author: str - """插件作者""" - description: str - """插件简介""" - version: Tuple[int, ...] - """插件版本号""" - type: PluginType - """插件类型""" - license: str = "MIT License" - """插件发布时采用的许可协议""" - dependencies: Sequence[str] = [] - """插件是否对其他插件存在依赖""" - - -class TopBasePlugin(ABC): - """所有插件的抽象基类""" - - metainfo: PluginMetaInformation - """插件元信息""" - - def __init_subclass__(cls) -> None: - super().__init_subclass__() - if hasattr(cls, "metainfo"): - if not isinstance(cls.metainfo, PluginMetaInformation): - raise PluginMetainfoTypeError( - "类`{cls_name}`之属性`metainfo`的类型,必须为`PluginMetaInformation`".format( - cls_name=cls.__name__ - ) - ) - else: - raise PluginMetainfoNotFoundError( - "类`{cls_name}`必须定义一个`metainfo`属性。".format( - cls_name=cls.__name__ - ) - ) - - -class TopInOutBasePlugin(TopBasePlugin, ABC): - """导入导出用抽象基类""" - - supported_formats: Tuple[str, ...] = tuple() - """支持的格式""" - - def __init_subclass__(cls) -> None: - super().__init_subclass__() - - if hasattr(cls, "supported_formats"): - if cls.supported_formats: - # 强制转换为大写,并使用元组 - cls.supported_formats = tuple(map(str.upper, cls.supported_formats)) - else: - cls.supported_formats = tuple() - else: - raise PluginAttributeNotFoundError( - "用于导入导出数据的类`{cls_name}`必须定义一个`supported_formats`属性。".format( - cls_name=cls.__name__ - ) - ) - - def can_handle_file(self, file_path: Path) -> bool: - """判断是否可处理某个文件""" - return file_path.suffix.upper().endswith(self.supported_formats) - - def can_handle_format(self, format_name: str) -> bool: - """判断是否可处理某个格式""" - return format_name.upper().endswith(self.supported_formats) - - -class MusicInputPlugin(TopInOutBasePlugin, ABC): - """导入用插件抽象基类-完整曲目""" - - def __init_subclass__(cls) -> None: - super().__init_subclass__() - - if cls.metainfo.type != PluginType.FUNCTION_IMPORT: - raise PluginMetainfoValueError( - "插件类`{cls_name}`是从`MusicInputPlugin`继承的,该类的子类应当为一个`PluginType.FUNCTION_IMPORT`类型的插件,而不是`PluginType.{cls_type}`".format( - cls_name=cls.__name__, - cls_type=cls.metainfo.type.name, - ) - ) - - @abstractmethod - def loadbytes( - self, bytes_buffer_in: BinaryIO, config: Optional[PluginConfig] - ) -> "SingleMusic": - """从字节流加载数据到完整曲目""" - pass - - def load(self, file_path: Path, config: Optional[PluginConfig]) -> "SingleMusic": - """从文件加载数据到完整曲目""" - with file_path.open("rb") as f: - return self.loadbytes(f, config) - - -class TrackInputPlugin(TopInOutBasePlugin, ABC): - """导入用插件抽象基类-单个音轨""" - - def __init_subclass__(cls) -> None: - super().__init_subclass__() - - if cls.metainfo.type != PluginType.FUNCTION_IMPORT: - raise PluginMetainfoValueError( - "插件类`{cls_name}`是从`TrackInputPlugin`继承的,该类的子类应当为一个`PluginType.FUNCTION_IMPORT`类型的插件,而不是`PluginType.{cls_type}`".format( - cls_name=cls.__name__, - cls_type=cls.metainfo.type.name, - ) - ) - - @abstractmethod - def loadbytes( - self, bytes_buffer_in: BinaryIO, config: Optional[PluginConfig] - ) -> "SingleTrack": - """从字节流加载音符数据到单个音轨""" - pass - - def load(self, file_path: Path, config: Optional[PluginConfig]) -> "SingleTrack": - """从文件加载音符数据到单个音轨""" - with file_path.open("rb") as f: - return self.loadbytes(f, config) - - -class MusicOperatePlugin(TopBasePlugin, ABC): - """音乐处理用插件抽象基类-完整曲目""" - - def __init_subclass__(cls) -> None: - super().__init_subclass__() - - if cls.metainfo.type != PluginType.FUNCTION_OPERATE: - raise PluginMetainfoValueError( - "插件类`{cls_name}`是从`MusicOperatePlugin`继承的,该类的子类应当为一个`PluginType.FUNCTION_OPERATE`类型的插件,而不是`PluginType.{cls_type}`".format( - cls_name=cls.__name__, - cls_type=cls.metainfo.type.name, - ) - ) - - @abstractmethod - def process( - self, data: "SingleMusic", config: Optional[PluginConfig] - ) -> "SingleMusic": - """处理完整曲目的数据""" - pass - - -class TrackOperatePlugin(TopBasePlugin, ABC): - """音乐处理用插件抽象基类-单个音轨""" - - def __init_subclass__(cls) -> None: - super().__init_subclass__() - - if cls.metainfo.type != PluginType.FUNCTION_OPERATE: - raise PluginMetainfoValueError( - "插件类`{cls_name}`是从`TrackOperatePlugin`继承的,该类的子类应当为一个`PluginType.FUNCTION_OPERATE`类型的插件,而不是`PluginType.{cls_type}`".format( - cls_name=cls.__name__, - cls_type=cls.metainfo.type.name, - ) - ) - - @abstractmethod - def process( - self, data: "SingleTrack", config: Optional[PluginConfig] - ) -> "SingleTrack": - """处理单个音轨的音符数据""" - pass - - -class MusicOutputPlugin(TopInOutBasePlugin, ABC): - """导出用插件的抽象基类-完整曲目""" - - def __init_subclass__(cls) -> None: - super().__init_subclass__() - - if cls.metainfo.type != PluginType.FUNCTION_EXPORT: - raise PluginMetainfoValueError( - "插件类`{cls_name}`是从`MusicOutputPlugin`继承的,该类的子类应当为一个`PluginType.FUNCTION_EXPORT`类型的插件,而不是`PluginType.{cls_type}`".format( - cls_name=cls.__name__, - cls_type=cls.metainfo.type.name, - ) - ) - - @abstractmethod - def dumpbytes( - self, data: "SingleMusic", config: Optional[PluginConfig] - ) -> BinaryIO: - """将完整曲目导出为对应格式的字节流""" - pass - - @abstractmethod - def dump( - self, data: "SingleMusic", file_path: Path, config: Optional[PluginConfig] - ): - """将完整曲目导出为对应格式的文件""" - pass - - -class TrackOutputPlugin(TopInOutBasePlugin, ABC): - """导出用插件的抽象基类-单个音轨""" - - def __init_subclass__(cls) -> None: - super().__init_subclass__() - - if cls.metainfo.type != PluginType.FUNCTION_EXPORT: - raise PluginMetainfoValueError( - "插件类`{cls_name}`是从`TrackOutputPlugin`继承的,该类的子类应当为一个`PluginType.FUNCTION_EXPORT`类型的插件,而不是`PluginType.{cls_type}`".format( - cls_name=cls.__name__, - cls_type=cls.metainfo.type.name, - ) - ) - - @abstractmethod - def dumpbytes( - self, data: "SingleTrack", config: Optional[PluginConfig] - ) -> BinaryIO: - """将单个音轨导出为对应格式的字节流""" - pass - - @abstractmethod - def dump( - self, data: "SingleTrack", file_path: Path, config: Optional[PluginConfig] - ): - """将单个音轨导出为对应格式的文件""" - pass - - -class ServicePlugin(TopBasePlugin, ABC): - """服务插件抽象基类""" - - def __init_subclass__(cls) -> None: - super().__init_subclass__() - - if cls.metainfo.type != PluginType.SERVICE: - raise PluginMetainfoValueError( - "插件类`{cls_name}`是从`ServicePlugin`继承的,该类的子类应当为一个`PluginType.SERVICE`类型的插件,而不是`PluginType.{cls_type}`".format( - cls_name=cls.__name__, - cls_type=cls.metainfo.type.name, - ) - ) - - @abstractmethod - def serve(self, config: Optional[PluginConfig], *args) -> None: - """服务插件的运行逻辑""" - pass - - -class LibraryPlugin(TopBasePlugin, ABC): - """插件依赖库的抽象基类""" - - def __init_subclass__(cls) -> None: - super().__init_subclass__() - - if cls.metainfo.type != PluginType.LIBRARY: - raise PluginMetainfoValueError( - "插件类`{cls_name}`是从`LibraryPlugin`继承的,该类的子类应当为一个`PluginType.LIBRARY`类型的插件,而不是`PluginType.{cls_type}`".format( - cls_name=cls.__name__, - cls_type=cls.metainfo.type.name, - ) - ) - - # 怎么? - # 插件的彼此依赖就不需要什么调用了吧 - - -class PluginRegistry: - """插件注册管理器""" - - def __init__(self): - self._music_input_plugins: List[MusicInputPlugin] = [] - self._track_input_plugins: List[TrackInputPlugin] = [] - self._music_operate_plugins: List[MusicOperatePlugin] = [] - self._track_operate_plugins: List[TrackOperatePlugin] = [] - self._music_output_plugins: List[MusicOutputPlugin] = [] - self._track_output_plugins: List[TrackOutputPlugin] = [] - self._service_plugins: List[ServicePlugin] = [] - self._library_plugins: List[LibraryPlugin] = [] - - def register_music_input_plugin(self, plugin_class: type) -> None: - """注册输入插件-整首曲目""" - plugin_instance = plugin_class() - self._music_input_plugins.append(plugin_instance) - - def register_track_input_plugin(self, plugin_class: type) -> None: - """注册输入插件-单个音轨""" - plugin_instance = plugin_class() - self._track_input_plugins.append(plugin_instance) - - def register_music_operate_plugin(self, plugin_class: type) -> None: - """注册曲目处理插件""" - plugin_instance = plugin_class() - self._music_operate_plugins.append(plugin_instance) - - def register_track_operate_plugin(self, plugin_class: type) -> None: - """注册音轨处理插件""" - plugin_instance = plugin_class() - self._track_operate_plugins.append(plugin_instance) - - def register_music_output_plugin(self, plugin_class: type) -> None: - """注册输出插件-整首曲目""" - plugin_instance = plugin_class() - self._music_output_plugins.append(plugin_instance) - - def register_track_output_plugin(self, plugin_class: type) -> None: - """注册输出插件-单个音轨""" - plugin_instance = plugin_class() - self._track_output_plugins.append(plugin_instance) - - def register_service_plugin(self, plugin_class: type) -> None: - """注册服务插件""" - plugin_instance = plugin_class() - self._service_plugins.append(plugin_instance) - - def register_library_plugin(self, plugin_class: type) -> None: - """注册支持库插件""" - plugin_instance = plugin_class() - self._library_plugins.append(plugin_instance) - - def get_music_input_plugin_by_format( - self, filepath_or_format: Union[Path, str] - ) -> Generator[MusicInputPlugin, None, None]: - """通过指定输入的文件或格式,以获取对应的全曲导入用插件""" - if isinstance(filepath_or_format, str): - for plugin in self._music_input_plugins: - if plugin.can_handle_format(filepath_or_format): - yield plugin - elif isinstance(filepath_or_format, Path): - for plugin in self._music_input_plugins: - if plugin.can_handle_file(filepath_or_format): - yield plugin - else: - raise ParameterTypeError( - "用于指定“导入全曲的数据之类型”的参数,其类型须为`Path`路径或字符串,而非`{}`类型的`{}`值".format( - type(filepath_or_format), filepath_or_format - ) - ) - - def get_track_input_plugin_by_format( - self, filepath_or_format: Union[Path, str] - ) -> Generator[TrackInputPlugin, None, None]: - """通过指定输入的文件或格式,以获取对应的单音轨导入用插件""" - if isinstance(filepath_or_format, str): - for plugin in self._track_input_plugins: - if plugin.can_handle_format(filepath_or_format): - yield plugin - elif isinstance(filepath_or_format, Path): - for plugin in self._track_input_plugins: - if plugin.can_handle_file(filepath_or_format): - yield plugin - else: - raise ParameterTypeError( - "用于指定“导入单个音轨的数据之类型”的参数,其类型须为`Path`路径或字符串,而非`{}`类型的`{}`值".format( - type(filepath_or_format), filepath_or_format - ) - ) - - def get_music_output_plugin_by_format( - self, filepath_or_format: Union[Path, str] - ) -> Generator[MusicOutputPlugin, None, None]: - """通过指定输出的文件或格式,以获取对应的导出全曲用插件""" - if isinstance(filepath_or_format, str): - for plugin in self._music_output_plugins: - if plugin.can_handle_format(filepath_or_format): - yield plugin - elif isinstance(filepath_or_format, Path): - for plugin in self._music_output_plugins: - if plugin.can_handle_file(filepath_or_format): - yield plugin - else: - raise ParameterTypeError( - "用于指定“全曲数据导出的类型”的参数,其类型须为`Path`路径或字符串,而非`{}`类型的`{}`值".format( - type(filepath_or_format), filepath_or_format - ) - ) - - def get_track_output_plugin_by_format( - self, filepath_or_format: Union[Path, str] - ) -> Generator[TrackOutputPlugin, None, None]: - """通过指定输出的文件或格式,以获取对应的导出单个音轨用插件""" - if isinstance(filepath_or_format, str): - for plugin in self._track_output_plugins: - if plugin.can_handle_format(filepath_or_format): - yield plugin - elif isinstance(filepath_or_format, Path): - for plugin in self._track_output_plugins: - if plugin.can_handle_file(filepath_or_format): - yield plugin - else: - raise ParameterTypeError( - "用于指定“单音轨数据导出的类型”的参数,其类型须为`Path`路径或字符串,而非`{}`类型的`{}`值".format( - type(filepath_or_format), filepath_or_format - ) - ) - - def get_music_input_plugin(self, plugin_name: str) -> MusicInputPlugin: - """获取指定名称的全曲导入用插件,当名称重叠时,取版本号最大的""" - try: - return max( - filter( - lambda plugin: plugin.metainfo.name == plugin_name, - self._music_input_plugins, - ), - key=lambda plugin: plugin.metainfo.version, - ) - except ValueError: - raise PluginInstanceNotFoundError( - "未找到“用于导入曲目、名为`{}`”的插件".format(plugin_name) - ) - - def get_track_input_plugin(self, plugin_name: str) -> TrackInputPlugin: - """获取指定名称的单音轨导入用插件,当名称重叠时,取版本号最大的""" - try: - return max( - filter( - lambda plugin: plugin.metainfo.name == plugin_name, - self._track_input_plugins, - ), - key=lambda plugin: plugin.metainfo.version, - ) - except ValueError: - raise PluginInstanceNotFoundError( - "未找到“用于导入单个音轨、名为`{}`”的插件".format(plugin_name) - ) - - def get_music_operate_plugin(self, plugin_name: str) -> MusicOperatePlugin: - """获取指定名称的全曲处理用插件,当名称重叠时,取版本号最大的""" - try: - return max( - filter( - lambda plugin: plugin.metainfo.name == plugin_name, - self._music_operate_plugins, - ), - key=lambda plugin: plugin.metainfo.version, - ) - except ValueError: - raise PluginInstanceNotFoundError( - "未找到“用于处理整个曲目、名为`{}`”的插件".format(plugin_name) - ) - - def get_track_operate_plugin(self, plugin_name: str) -> TrackOperatePlugin: - """获取指定名称的单音轨处理用插件,当名称重叠时,取版本号最大的""" - try: - return max( - filter( - lambda plugin: plugin.metainfo.name == plugin_name, - self._track_operate_plugins, - ), - key=lambda plugin: plugin.metainfo.version, - ) - except ValueError: - raise PluginInstanceNotFoundError( - "未找到“用于处理单个音轨、名为`{}`”的插件".format(plugin_name) - ) - - def get_music_output_plugin(self, plugin_name: str) -> MusicOutputPlugin: - """获取指定名称的导出全曲用插件,当名称重叠时,取版本号最大的""" - try: - return max( - filter( - lambda plugin: plugin.metainfo.name == plugin_name, - self._music_output_plugins, - ), - key=lambda plugin: plugin.metainfo.version, - ) - except ValueError: - raise PluginMetainfoNotFoundError( - "未找到“用于导出完整曲目、名为`{}`”的插件".format(plugin_name) - ) - - def get_track_output_plugin(self, plugin_name: str) -> TrackOutputPlugin: - """获取指定名称的导出单音轨用插件,当名称重叠时,取版本号最大的""" - try: - return max( - filter( - lambda plugin: plugin.metainfo.name == plugin_name, - self._track_output_plugins, - ), - key=lambda plugin: plugin.metainfo.version, - ) - except ValueError: - raise PluginMetainfoNotFoundError( - "未找到“用于导出单个音轨、名为`{}`”的插件".format(plugin_name) - ) - - def get_service_plugin(self, plugin_name: str) -> ServicePlugin: - """获取服务用插件,当名称重叠时,取版本号最大的""" - try: - return max( - filter( - lambda plugin: plugin.metainfo.name == plugin_name, - self._service_plugins, - ), - key=lambda plugin: plugin.metainfo.version, - ) - except ValueError: - raise PluginInstanceNotFoundError( - "未找到名为`{}`的服务用插件".format(plugin_name) - ) - - def get_library_plugin(self, plugin_name: str) -> LibraryPlugin: - """获取依赖库类插件,当名称重叠时,取版本号最高的""" - try: - return max( - filter( - lambda plugin: plugin.metainfo.name == plugin_name, - self._library_plugins, - ), - key=lambda plugin: plugin.metainfo.version, - ) - except ValueError: - raise PluginInstanceNotFoundError( - "未找到名为`{}`的依赖库插件".format(plugin_name) - ) - - def supported_input_formats(self) -> Set[str]: - """所有支持的导入格式""" - return set( - chain.from_iterable( - plugin.supported_formats - for plugin in chain( - self._music_input_plugins, self._track_input_plugins - ) - ) - ) - - def supported_output_formats(self) -> Set[str]: - """所有支持的导出格式""" - return set( - chain.from_iterable( - plugin.supported_formats - for plugin in chain( - self._music_output_plugins, self._track_output_plugins - ) - ) - ) - - -plugin_registry = PluginRegistry() -"""全局插件注册表实例""" - - -def music_input_plugin(metainfo: PluginMetaInformation): - """全曲输入用插件装饰器""" - - def decorator(cls): - global plugin_registry - cls.metainfo = metainfo - plugin_registry.register_music_input_plugin(cls) - return cls - - return decorator - - -def track_input_plugin(metainfo: PluginMetaInformation): - """单轨输入用插件装饰器""" - - def decorator(cls): - global plugin_registry - cls.metainfo = metainfo - plugin_registry.register_track_input_plugin(cls) - return cls - - return decorator - - -def music_operate_plugin(metainfo: PluginMetaInformation): - """全曲处理用插件装饰器""" - - def decorator(cls): - global plugin_registry - cls.metainfo = metainfo - plugin_registry.register_music_operate_plugin(cls) - return cls - - return decorator - - -def track_operate_plugin(metainfo: PluginMetaInformation): - """音轨处理插件装饰器""" - - def decorator(cls): - global plugin_registry - cls.metainfo = metainfo - plugin_registry.register_track_operate_plugin(cls) - return cls - - return decorator - - -def music_output_plugin(metainfo: PluginMetaInformation): - """乐曲输出用插件装饰器""" - - def decorator(cls): - global plugin_registry - cls.metainfo = metainfo - plugin_registry.register_music_output_plugin(cls) - return cls - - return decorator - - -def track_output_plugin(metainfo: PluginMetaInformation): - """音轨输出用插件装饰器""" - - def decorator(cls): - global plugin_registry - cls.metainfo = metainfo - plugin_registry.register_track_output_plugin(cls) - return cls - - return decorator - - -def service_plugin(metainfo: PluginMetaInformation): - """服务插件装饰器""" - - def decorator(cls): - global plugin_registry - cls.metainfo = metainfo - plugin_registry.register_service_plugin(cls) - return cls - - return decorator - - -def library_plugin(metainfo: PluginMetaInformation): - """支持库插件装饰器""" - - def decorator(cls): - global plugin_registry - cls.metainfo = metainfo - plugin_registry.register_library_plugin(cls) - return cls - - return decorator diff --git a/Musicreater/plugins.py b/Musicreater/plugins.py index c8ceb0e..2563fd4 100644 --- a/Musicreater/plugins.py +++ b/Musicreater/plugins.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """ -存储 音·创 v3 的插件管理和上层设计内容 +存储 音·创 v3 的插件接口与管理相关内容 """ """ @@ -16,7 +16,471 @@ Terms & Conditions: License.md in the root directory # Email TriM-Organization@hotmail.com # 若需转载或借鉴 许可声明请查看仓库目录下的 License.md -from typing import List, Optional, Dict, Generator, Any -from pathlib import Path -from .plugin import MusicInputPlugin, MusicOperatePlugin, MusicOutputPlugin, TrackInputPlugin, TrackOperatePlugin, TrackOutputPlugin, ServicePlugin, LibraryPlugin +import importlib +from pathlib import Path +from typing import Dict, Any, Optional, List, Tuple, Union, Generator, Set +from itertools import chain + + +from ._plugin_abc import ( + # 枚举类 + PluginType, + # 抽象基类/数据类(插件参数定义) + PluginConfig, + PluginMetaInformation, + # 抽象基类(插件定义) + MusicInputPlugin, + TrackInputPlugin, + MusicOperatePlugin, + TrackOperatePlugin, + MusicOutputPlugin, + TrackOutputPlugin, + ServicePlugin, + LibraryPlugin, +) +from .exceptions import ( + PluginMetainfoNotFoundError, + ParameterTypeError, + PluginInstanceNotFoundError, +) + + +__all__ = [ + # 枚举类 + "PluginType", + # 抽象基类/数据类(插件参数定义) + "PluginConfig", + "PluginMetaInformation", + # 抽象基类(插件定义) + "MusicInputPlugin", + "TrackInputPlugin", + "MusicOperatePlugin", + "TrackOperatePlugin", + "MusicOutputPlugin", + "TrackOutputPlugin", + "ServicePlugin", + "LibraryPlugin", + # 插件注册用装饰函数 + "music_input_plugin", + "track_input_plugin", + "music_operate_plugin", + "track_operate_plugin", + "music_output_plugin", + "track_output_plugin", + "service_plugin", + "library_plugin", +] + + +def load_plugin_module(package: Union[Path, str]): + """自动发现并加载插件包中的插件 + + 参数: + ===== + package: 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])) + else: + return importlib.import_module( + ".".join(relative_path.parts[:-1] + (relative_path.stem,)) + ) + else: + return importlib.import_module(package) + + +class PluginRegistry: + """插件注册管理器(注册表)""" + + def __init__(self): + # 实际上在纵容那些有着同样名称的插件…… + # (不用 Dict[str`plugin name`, PluginClass`] 的形式) + # 啊,我真的很高尚 + # 你真的不会把插件注册两遍吧……对吧? + self._music_input_plugins: Set[MusicInputPlugin] = set() + self._track_input_plugins: Set[TrackInputPlugin] = set() + self._music_operate_plugins: Set[MusicOperatePlugin] = set() + self._track_operate_plugins: Set[TrackOperatePlugin] = set() + self._music_output_plugins: Set[MusicOutputPlugin] = set() + self._track_output_plugins: Set[TrackOutputPlugin] = set() + self._service_plugins: Set[ServicePlugin] = set() + self._library_plugins: Set[LibraryPlugin] = set() + + def register_music_input_plugin(self, plugin_class: type) -> None: + """注册输入插件-整首曲目""" + self._music_input_plugins.add(plugin_class()) + + def register_track_input_plugin(self, plugin_class: type) -> None: + """注册输入插件-单个音轨""" + self._track_input_plugins.add(plugin_class()) + + def register_music_operate_plugin(self, plugin_class: type) -> None: + """注册曲目处理插件""" + self._music_operate_plugins.add(plugin_class()) + + def register_track_operate_plugin(self, plugin_class: type) -> None: + """注册音轨处理插件""" + self._track_operate_plugins.add(plugin_class()) + + def register_music_output_plugin(self, plugin_class: type) -> None: + """注册输出插件-整首曲目""" + self._music_output_plugins.add(plugin_class()) + + def register_track_output_plugin(self, plugin_class: type) -> None: + """注册输出插件-单个音轨""" + self._track_output_plugins.add(plugin_class()) + + def register_service_plugin(self, plugin_class: type) -> None: + """注册服务插件""" + self._service_plugins.add(plugin_class()) + + def register_library_plugin(self, plugin_class: type) -> None: + """注册支持库插件""" + self._library_plugins.add(plugin_class()) + + def get_music_input_plugin_by_format( + self, filepath_or_format: Union[Path, str] + ) -> Generator[MusicInputPlugin, None, None]: + """通过指定输入的文件或格式,以获取对应的全曲导入用插件""" + if isinstance(filepath_or_format, str): + return ( + plugin + for plugin in self._music_input_plugins + if plugin.can_handle_format(filepath_or_format) + ) + elif isinstance(filepath_or_format, Path): + return ( + plugin + for plugin in self._music_input_plugins + if plugin.can_handle_file(filepath_or_format) + ) + else: + raise ParameterTypeError( + "用于指定“导入全曲的数据之类型”的参数,其类型须为`Path`路径或字符串,而非`{}`类型的`{}`值".format( + type(filepath_or_format), filepath_or_format + ) + ) + + def get_track_input_plugin_by_format( + self, filepath_or_format: Union[Path, str] + ) -> Generator[TrackInputPlugin, None, None]: + """通过指定输入的文件或格式,以获取对应的单音轨导入用插件""" + if isinstance(filepath_or_format, str): + return ( + plugin + for plugin in self._track_input_plugins + if plugin.can_handle_format(filepath_or_format) + ) + elif isinstance(filepath_or_format, Path): + return ( + plugin + for plugin in self._track_input_plugins + if plugin.can_handle_file(filepath_or_format) + ) + else: + raise ParameterTypeError( + "用于指定“导入单个音轨的数据之类型”的参数,其类型须为`Path`路径或字符串,而非`{}`类型的`{}`值".format( + type(filepath_or_format), filepath_or_format + ) + ) + + def get_music_output_plugin_by_format( + self, filepath_or_format: Union[Path, str] + ) -> Generator[MusicOutputPlugin, None, None]: + """通过指定输出的文件或格式,以获取对应的导出全曲用插件""" + if isinstance(filepath_or_format, str): + return ( + plugin + for plugin in self._music_output_plugins + if plugin.can_handle_format(filepath_or_format) + ) + elif isinstance(filepath_or_format, Path): + return ( + plugin + for plugin in self._music_output_plugins + if plugin.can_handle_file(filepath_or_format) + ) + else: + raise ParameterTypeError( + "用于指定“全曲数据导出的类型”的参数,其类型须为`Path`路径或字符串,而非`{}`类型的`{}`值".format( + type(filepath_or_format), filepath_or_format + ) + ) + + def get_track_output_plugin_by_format( + self, filepath_or_format: Union[Path, str] + ) -> Generator[TrackOutputPlugin, None, None]: + """通过指定输出的文件或格式,以获取对应的导出单个音轨用插件""" + if isinstance(filepath_or_format, str): + return ( + plugin + for plugin in self._track_output_plugins + if plugin.can_handle_format(filepath_or_format) + ) + elif isinstance(filepath_or_format, Path): + return ( + plugin + for plugin in self._track_output_plugins + if plugin.can_handle_file(filepath_or_format) + ) + else: + raise ParameterTypeError( + "用于指定“单音轨数据导出的类型”的参数,其类型须为`Path`路径或字符串,而非`{}`类型的`{}`值".format( + type(filepath_or_format), filepath_or_format + ) + ) + + def get_music_input_plugin(self, plugin_name: str) -> MusicInputPlugin: + """获取指定名称的全曲导入用插件,当名称重叠时,取版本号最大的""" + try: + return max( + [ + plugin + for plugin in self._music_input_plugins + if plugin.metainfo.name == plugin_name + ], + key=lambda plugin: plugin.metainfo.version, + ) + except ValueError: + raise PluginInstanceNotFoundError( + "未找到“用于导入曲目、名为`{}`”的插件".format(plugin_name) + ) + + def get_track_input_plugin(self, plugin_name: str) -> TrackInputPlugin: + """获取指定名称的单音轨导入用插件,当名称重叠时,取版本号最大的""" + try: + return max( + [ + plugin + for plugin in self._track_input_plugins + if plugin.metainfo.name == plugin_name + ], + key=lambda plugin: plugin.metainfo.version, + ) + except ValueError: + raise PluginInstanceNotFoundError( + "未找到“用于导入单个音轨、名为`{}`”的插件".format(plugin_name) + ) + + def get_music_operate_plugin(self, plugin_name: str) -> MusicOperatePlugin: + """获取指定名称的全曲处理用插件,当名称重叠时,取版本号最大的""" + try: + return max( + [ + plugin + for plugin in self._music_operate_plugins + if plugin.metainfo.name == plugin_name + ], + key=lambda plugin: plugin.metainfo.version, + ) + except ValueError: + raise PluginInstanceNotFoundError( + "未找到“用于处理整个曲目、名为`{}`”的插件".format(plugin_name) + ) + + def get_track_operate_plugin(self, plugin_name: str) -> TrackOperatePlugin: + """获取指定名称的单音轨处理用插件,当名称重叠时,取版本号最大的""" + try: + return max( + [ + plugin + for plugin in self._track_operate_plugins + if plugin.metainfo.name == plugin_name + ], + key=lambda plugin: plugin.metainfo.version, + ) + except ValueError: + raise PluginInstanceNotFoundError( + "未找到“用于处理单个音轨、名为`{}`”的插件".format(plugin_name) + ) + + def get_music_output_plugin(self, plugin_name: str) -> MusicOutputPlugin: + """获取指定名称的导出全曲用插件,当名称重叠时,取版本号最大的""" + try: + return max( + [ + plugin + for plugin in self._music_output_plugins + if plugin.metainfo.name == plugin_name + ], + key=lambda plugin: plugin.metainfo.version, + ) + except ValueError: + raise PluginMetainfoNotFoundError( + "未找到“用于导出完整曲目、名为`{}`”的插件".format(plugin_name) + ) + + def get_track_output_plugin(self, plugin_name: str) -> TrackOutputPlugin: + """获取指定名称的导出单音轨用插件,当名称重叠时,取版本号最大的""" + try: + return max( + [ + plugin + for plugin in self._track_output_plugins + if plugin.metainfo.name == plugin_name + ], + key=lambda plugin: plugin.metainfo.version, + ) + except ValueError: + raise PluginMetainfoNotFoundError( + "未找到“用于导出单个音轨、名为`{}`”的插件".format(plugin_name) + ) + + def get_service_plugin(self, plugin_name: str) -> ServicePlugin: + """获取服务用插件,当名称重叠时,取版本号最大的""" + try: + return max( + [ + plugin + for plugin in self._service_plugins + if plugin.metainfo.name == plugin_name + ], + key=lambda plugin: plugin.metainfo.version, + ) + except ValueError: + raise PluginInstanceNotFoundError( + "未找到名为`{}`的服务用插件".format(plugin_name) + ) + + def get_library_plugin(self, plugin_name: str) -> LibraryPlugin: + """获取依赖库类插件,当名称重叠时,取版本号最高的""" + try: + return max( + [ + plugin + for plugin in self._library_plugins + if plugin.metainfo.name == plugin_name + ], + key=lambda plugin: plugin.metainfo.version, + ) + except ValueError: + raise PluginInstanceNotFoundError( + "未找到名为`{}`的依赖库插件".format(plugin_name) + ) + + def supported_input_formats(self) -> Set[str]: + """所有支持的导入格式""" + return set( + chain.from_iterable( + plugin.supported_formats + for plugin in chain( + self._music_input_plugins, self._track_input_plugins + ) + ) + ) + + def supported_output_formats(self) -> Set[str]: + """所有支持的导出格式""" + return set( + chain.from_iterable( + plugin.supported_formats + for plugin in chain( + self._music_output_plugins, self._track_output_plugins + ) + ) + ) + + +__global_plugin_registry = PluginRegistry() +"""全局插件注册表实例""" + + +def music_input_plugin(metainfo: PluginMetaInformation): + """全曲输入用插件装饰器""" + + def decorator(cls): + global __global_plugin_registry + cls.metainfo = metainfo + __global_plugin_registry.register_music_input_plugin(cls) + return cls + + return decorator + + +def track_input_plugin(metainfo: PluginMetaInformation): + """单轨输入用插件装饰器""" + + def decorator(cls): + global __global_plugin_registry + cls.metainfo = metainfo + __global_plugin_registry.register_track_input_plugin(cls) + return cls + + return decorator + + +def music_operate_plugin(metainfo: PluginMetaInformation): + """全曲处理用插件装饰器""" + + def decorator(cls): + global __global_plugin_registry + cls.metainfo = metainfo + __global_plugin_registry.register_music_operate_plugin(cls) + return cls + + return decorator + + +def track_operate_plugin(metainfo: PluginMetaInformation): + """音轨处理插件装饰器""" + + def decorator(cls): + global __global_plugin_registry + cls.metainfo = metainfo + __global_plugin_registry.register_track_operate_plugin(cls) + return cls + + return decorator + + +def music_output_plugin(metainfo: PluginMetaInformation): + """乐曲输出用插件装饰器""" + + def decorator(cls): + global __global_plugin_registry + cls.metainfo = metainfo + __global_plugin_registry.register_music_output_plugin(cls) + return cls + + return decorator + + +def track_output_plugin(metainfo: PluginMetaInformation): + """音轨输出用插件装饰器""" + + def decorator(cls): + global __global_plugin_registry + cls.metainfo = metainfo + __global_plugin_registry.register_track_output_plugin(cls) + return cls + + return decorator + + +def service_plugin(metainfo: PluginMetaInformation): + """服务插件装饰器""" + + def decorator(cls): + global __global_plugin_registry + cls.metainfo = metainfo + __global_plugin_registry.register_service_plugin(cls) + return cls + + return decorator + + +def library_plugin(metainfo: PluginMetaInformation): + """支持库插件装饰器""" + + def decorator(cls): + global __global_plugin_registry + cls.metainfo = metainfo + __global_plugin_registry.register_library_plugin(cls) + return cls + + return decorator diff --git a/Musicreater/types.py b/Musicreater/types.py index 8ebd4b8..6cd114a 100644 --- a/Musicreater/types.py +++ b/Musicreater/types.py @@ -5,8 +5,8 @@ """ """ -版权所有 © 2025 金羿 & 玉衡 -Copyright © 2025 Eilles & Alioth +版权所有 © 2025 金羿 & 玉衡Alioth +Copyright © 2025 Eilles & YuhengAlioth 开源相关声明请见 仓库根目录下的 License.md Terms & Conditions: License.md in the root directory diff --git a/Packer/MSCT_Packer.py b/Packer/MSCT_Packer.py index 50e549f..283bc82 100644 --- a/Packer/MSCT_Packer.py +++ b/Packer/MSCT_Packer.py @@ -1,4 +1,4 @@ -import Musicreater +import Musicreater.old_init as old_init import Musicreater.experiment import Musicreater.old_plugin @@ -16,12 +16,12 @@ from Musicreater.old_plugin.mcstructfile import ( ) MSCT_MAIN = ( - Musicreater, - Musicreater.experiment, + old_init, + old_init.experiment, # Musicreater.previous, ) -MSCT_PLUGIN = (Musicreater.old_plugin,) +MSCT_PLUGIN = (old_init.old_plugin,) MSCT_PLUGIN_FUNCTION = ( to_addon_pack_in_delay, diff --git a/README.md b/README.md index 14f761a..4a77550 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ [Bilibili: 金羿ELS]: https://img.shields.io/badge/Bilibili-%E9%87%91%E7%BE%BFELS-00A1E7?style=for-the-badge -[Bilibili: 诸葛亮与八卦阵]: https://img.shields.io/badge/Bilibili-%E8%AF%B8%E8%91%9B%E4%BA%AE%E4%B8%8E%E5%85%AB%E5%8D%A6%E9%98%B5-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 [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 @@ -23,7 +23,7 @@

[![][Bilibili: 金羿ELS]](https://space.bilibili.com/397369002/) -[![][Bilibili: 诸葛亮与八卦阵]](https://space.bilibili.com/604072474) +[![][Bilibili: 玉衡Alioth]](https://space.bilibili.com/604072474) [![CodeStyle: black]](https://github.com/psf/black) [![][python]](https://www.python.org/) [![][license]](LICENSE) @@ -83,7 +83,7 @@ **金羿 Eilles**:我的世界基岩版指令作者,个人开发者,B 站不知名 UP 主。 -**诸葛亮与八卦阵 bgArray**:我的世界基岩版玩家,喜欢编程和音乐,深圳学生。 +**玉衡Alioth Alioth**:我的世界基岩版玩家,喜欢编程和音乐,学生。 **偷吃不是Touch Touch**:我的世界基岩版指令制作者,提供测试支持 @@ -97,7 +97,7 @@ - 感谢由 **[Dislink Sforza](https://github.com/Dislink) “断联·斯福尔扎”**\ 带来的 midi 音色解析以及转换指令的算法,我们将其改编并应用;同时,感谢他的[网页版转换器](https://dislink.github.io/midi2bdx/)给我们的开发与更新带来巨大的压力和动力,让我们在原本一骑绝尘的摸鱼道路上转向开发。 - 感谢 **Mono**\ 反馈安装时的问题,辅助我们找到了视窗操作系统下的兼容性问题;感谢其反馈延迟播放器出现的重大问题,让我们得以修改全部延迟播放错误;尤其感谢他对于我们的软件的大力宣传 - 感谢 **Ammelia “艾米利亚”**\ 敦促我们进行新的功能开发,并为新功能提出了非常优秀的大量建议,以及提供的 BDX 导入测试支持,为我们的新结构生成算法提供了大量的实际理论支持 -- 感谢 **[神羽](https://gitee.com/snowykami) “[SnowyKami](https://github.com/snowyfirefly)”** 对我们项目的支持与宣传,非常感谢他为我们提供的服务器! +- 感谢 **[神羽 “SnowyKami”](https://www.sfkm.me/)** 对我们项目的支持与宣传,非常感谢他为我们提供的服务器! - 感谢 **指令师\_苦力怕 playjuice123**\ 为我们的程序找出错误,并提醒我们修复一个一直存在的大 bug。 - 感谢 **雷霆**\ 用他那令所有开发者都大为光火的操作方法为我们的程序找出错误,并提醒修复 bug。 - 感谢 **小埋**\ 反馈附加包生成时缺少描述和标题的问题。 diff --git a/README_EN.md b/README_EN.md index 77d6f1c..7444c14 100644 --- a/README_EN.md +++ b/README_EN.md @@ -1,5 +1,5 @@ -[Bilibili: Eilles]: https://img.shields.io/badge/Bilibili-%E9%87%91%E7%BE%BFELS-00A1E7?style=for-the-badge -[Bilibili: bgArray]: https://img.shields.io/badge/Bilibili-%E8%AF%B8%E8%91%9B%E4%BA%AE%E4%B8%8E%E5%85%AB%E5%8D%A6%E9%98%B5-00A1E7?style=for-the-badge +[Bilibili: Eilles]: https://img.shields.io/badge/Bilibili-%E9%87%91%E7%BE%BFEilles-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 [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 @@ -14,7 +14,7 @@

-

A free open-source library of Minecraft digital music.

+

A free and open-source library for handling with Minecraft digital music.

@@ -22,7 +22,7 @@

[![][Bilibili: Eilles]](https://space.bilibili.com/397369002/) -[![][Bilibili: bgArray]](https://space.bilibili.com/604072474) +[![][Bilibili: Alioth]](https://space.bilibili.com/604072474) [![CodeStyle: black]](https://github.com/psf/black) [![][python]](https://www.python.org/) [![][license]](LICENSE) @@ -39,7 +39,7 @@ ## Introduction🚀 -Musicreater is a free open-source library used for digital music that being played in _Minecraft_. +Musicreater is a free open-source library used for handling with digital musics that being able to be played in _Minecraft_. Welcome to join our QQ group: [861684859](https://jq.qq.com/?_wv=1027&k=hpeRxrYr) @@ -85,7 +85,7 @@ Commands such as `python`、`pip` could be changed to some like `python3` or `pi **Eilles (金羿)**:A student, individual developer, unfamous Bilibili UPer, which knows a little about commands in _Minecraft: Bedrock Edition_ -**bgArray (诸葛亮与八卦阵)**: A student, player of _Minecraft: Bedrock Edition_, which is a fan of music and programming. +**Alioth (玉衡Alioth)**: A student, player of _Minecraft: Bedrock Edition_, which is a fan of music and programming. **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 diff --git a/example.py b/example.py index 015daa6..f1ea697 100644 --- a/example.py +++ b/example.py @@ -18,7 +18,7 @@ Terms & Conditions: ./License.md import os -import Musicreater +import Musicreater.old_init as old_init from Musicreater.old_plugin.addonpack import ( to_addon_pack_in_delay, to_addon_pack_in_repeater, @@ -156,7 +156,7 @@ else: print(f"正在处理 {midi_path} :") -cvt_mid = Musicreater.MidiConvert.from_midi_file( +cvt_mid = old_init.MidiConvert.from_midi_file( midi_path, old_exe_format=False, min_volume=prompts[0], play_speed=prompts[1] ) @@ -187,7 +187,7 @@ print( cvt_method( cvt_mid, out_path, - Musicreater.DEFAULT_PROGRESSBAR_STYLE if prompts[2] else None, # type: ignore + old_init.DEFAULT_PROGRESSBAR_STYLE if prompts[2] else None, # type: ignore *prompts[3:], ) ) @@ -199,14 +199,14 @@ print( to_BDX_file_in_score( cvt_mid, out_path, - Musicreater.DEFAULT_PROGRESSBAR_STYLE if prompts[2] else None, + old_init.DEFAULT_PROGRESSBAR_STYLE if prompts[2] else None, *prompts[3:], ) if playerFormat == 1 else to_BDX_file_in_delay( cvt_mid, out_path, - Musicreater.DEFAULT_PROGRESSBAR_STYLE if prompts[2] else None, + old_init.DEFAULT_PROGRESSBAR_STYLE if prompts[2] else None, *prompts[3:], ) ) diff --git a/example_singleConvert.py b/example_singleConvert.py index 27098e1..4657bed 100644 --- a/example_singleConvert.py +++ b/example_singleConvert.py @@ -1,10 +1,10 @@ -import Musicreater +import Musicreater.old_init as old_init import Musicreater.old_plugin import Musicreater.old_plugin.mcstructfile print( - Musicreater.old_plugin.mcstructfile.to_mcstructure_file_in_delay( - Musicreater.MidiConvert.from_midi_file( + old_init.old_plugin.mcstructfile.to_mcstructure_file_in_delay( + old_init.MidiConvert.from_midi_file( input("midi路径:"), old_exe_format=False, # note_table_replacement={"note.harp": "note.flute"}, diff --git a/example_websocket.py b/example_websocket.py index 7f5cdda..15f1fae 100644 --- a/example_websocket.py +++ b/example_websocket.py @@ -1,4 +1,4 @@ -import Musicreater +import Musicreater.old_init as old_init import Musicreater.old_plugin import Musicreater.old_plugin.websocket @@ -7,9 +7,9 @@ import os dire = input("midi目录:") print( - Musicreater.old_plugin.websocket.to_websocket_server( + old_init.old_plugin.websocket.to_websocket_server( [ - Musicreater.MidiConvert.from_midi_file( + old_init.MidiConvert.from_midi_file( os.path.join(dire, names), old_exe_format=False ) for names in os.listdir( @@ -19,6 +19,6 @@ print( ], input("服务器地址:"), int(input("服务器端口:")), - Musicreater.DEFAULT_PROGRESSBAR_STYLE, + old_init.DEFAULT_PROGRESSBAR_STYLE, ) ) diff --git a/let_future_java.py b/let_future_java.py index 12ffb3c..9fb519c 100644 --- a/let_future_java.py +++ b/let_future_java.py @@ -123,7 +123,7 @@ msc_cvt = Musicreater.experiment.FutureMidiConvertJavaE.from_midi_file( input("midi路径:"), play_speed=float(input("播放速度:")), old_exe_format=True, - note_table_replacement=Musicreater.MC_EILLES_RTJE12_INSTRUMENT_REPLACE_TABLE, + note_table_replacement=Musicreater.old_init.MC_EILLES_RTJE12_INSTRUMENT_REPLACE_TABLE, # pitched_note_table=Musicreater.MM_NBS_PITCHED_INSTRUMENT_TABLE, ) diff --git a/test_fsq_opera.py b/test_fsq_opera.py index e6f2a96..646756a 100644 --- a/test_fsq_opera.py +++ b/test_fsq_opera.py @@ -1,13 +1,13 @@ from rich.pretty import pprint -import Musicreater +import Musicreater.old_init as old_init from Musicreater.utils import ( load_decode_fsq_flush_release, load_decode_musicsequence_metainfo, ) -msc_seq = Musicreater.MusicSequence.from_mido( - Musicreater.mido.MidiFile( +msc_seq = old_init.MusicSequence.from_mido( + old_init.mido.MidiFile( "./resources/测试片段.mid", ), "TEST-测试片段", @@ -20,7 +20,7 @@ with open("test.fsq", "wb") as f: f.write(fsq_bytes := msc_seq.encode_dump(flowing_codec_support=True)) with open("test.fsq", "rb") as f: - msc_seq_r = Musicreater.MusicSequence.load_decode(f.read(), verify=True) + msc_seq_r = old_init.MusicSequence.load_decode(f.read(), verify=True) pprint("FSQ 传入类成功:") pprint(msc_seq_r) diff --git a/test_msq_opera.py b/test_msq_opera.py index 45f6588..79d7beb 100644 --- a/test_msq_opera.py +++ b/test_msq_opera.py @@ -1,13 +1,13 @@ from rich.pretty import pprint -import Musicreater +import Musicreater.old_init as old_init from Musicreater.utils import ( load_decode_msq_flush_release, load_decode_musicsequence_metainfo, ) -msc_seq = Musicreater.MusicSequence.from_mido( - Musicreater.mido.MidiFile( +msc_seq = old_init.MusicSequence.from_mido( + old_init.mido.MidiFile( "./resources/测试片段.mid", ), "TEST-测试片段", @@ -20,7 +20,7 @@ with open("test.msq", "wb") as f: f.write(msq_bytes := msc_seq.encode_dump()) with open("test.msq", "rb") as f: - msc_seq_r = Musicreater.MusicSequence.load_decode(f.read()) + msc_seq_r = old_init.MusicSequence.load_decode(f.read()) pprint("常规 MSQ 读取成功:") pprint(msc_seq_r) diff --git a/tests/genexpr_vs_yieldfrom.py b/tests/genexpr_vs_yieldfrom.py new file mode 100644 index 0000000..9497015 --- /dev/null +++ b/tests/genexpr_vs_yieldfrom.py @@ -0,0 +1,21 @@ + +# 模拟两种写法 +def method_A(self, start, end): + yield from (f"{track}.get_range(start, end)" for track in self) + +def method_B(self, start, end): + return (f"{track}.get_range(start, end)" for track in self) + + + +tracks = ["A", "B"] + +gen_a = method_A(tracks, 0, 10) +print(list(gen_a)) + + + +gen_b = method_B(tracks, 0, 10) +print(list(gen_b)) + +# they are the same output