完美,同志,完美!!!!!!!

This commit is contained in:
2026-02-04 00:06:14 +08:00
parent 841f6e53c6
commit 1d9931f79d
69 changed files with 1716 additions and 209 deletions

View File

@@ -27,7 +27,6 @@ https://gitee.com/TriM-Organization/Musicreater/blob/master/LICENSE.md。
# 若需转载或借鉴 许可声明请查看仓库目录下的 License.md # 若需转载或借鉴 许可声明请查看仓库目录下的 License.md
__version__ = "3.0.0-alpha" __version__ = "3.0.0-alpha"
__author__ = ( __author__ = (
@@ -36,3 +35,37 @@ __author__ = (
("鱼旧梦", "ElapsingDreams"), ("鱼旧梦", "ElapsingDreams"),
("偷吃不是Touch", "Touch"), ("偷吃不是Touch", "Touch"),
) )
from .paramcurve import ParamCurve, InterpolationMethod, BoundaryBehaviour
from .data import (
SingleMusic,
SingleTrack,
SingleNote,
SoundAtmos,
MineNote,
CurvableParam,
)
from .plugins import load_plugin_module
from .main import MusiCreater
__all__ = [
"__version__",
"__author__",
# 参数曲线相关
"ParamCurve",
"InterpolationMethod",
"BoundaryBehaviour",
# 音乐数据结构
"SingleMusic",
"SingleTrack",
"SingleNote",
"SoundAtmos",
"MineNote",
"CurvableParam",
# 工程项目相关
"load_plugin_module",
"MusiCreater",
]

View File

@@ -5,8 +5,8 @@
""" """
""" """
版权所有 © 2025 金羿 版权所有 © 2026 金羿
Copyright © 2025 Eilles Copyright © 2026 Eilles
开源相关声明请见 仓库根目录下的 License.md 开源相关声明请见 仓库根目录下的 License.md
Terms & Conditions: License.md in the root directory Terms & Conditions: License.md in the root directory
@@ -26,7 +26,7 @@ Terms & Conditions: License.md in the root directory
import sys import sys
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from dataclasses import dataclass from dataclasses import dataclass, field
from enum import Enum from enum import Enum
from pathlib import Path from pathlib import Path
from typing import ( from typing import (
@@ -137,9 +137,12 @@ class PluginConfig(ABC):
class PluginType(str, Enum): class PluginType(str, Enum):
"""插件类型枚举""" """插件类型枚举"""
FUNCTION_IMPORT = "import_data" FUNCTION_MUSIC_IMPORT = "import_music_data"
FUNCTION_EXPORT = "export_data" FUNCTION_TRACK_IMPORT = "import_track_data"
FUNCTION_OPERATE = "data_operate" FUNCTION_MUSIC_OPERATE = "music_data_operating"
FUNCTION_TRACK_OPERATE = "track_data_operating"
FUNCTION_MUSIC_EXPORT = "export_music_data"
FUNCTION_TRACK_EXPORT = "export_track_data"
SERVICE = "service" SERVICE = "service"
LIBRARY = "library" LIBRARY = "library"
@@ -160,11 +163,11 @@ class PluginMetaInformation(ABC):
"""插件类型""" """插件类型"""
license: str = "MIT License" license: str = "MIT License"
"""插件发布时采用的许可协议""" """插件发布时采用的许可协议"""
dependencies: Sequence[str] = [] dependencies: Sequence[str] = tuple()
"""插件是否对其他插件存在依赖""" """插件是否对其他插件存在依赖"""
class TopBasePlugin(ABC): class TopPluginBase(ABC):
"""所有插件的抽象基类""" """所有插件的抽象基类"""
metainfo: PluginMetaInformation metainfo: PluginMetaInformation
@@ -180,14 +183,15 @@ class TopBasePlugin(ABC):
) )
) )
else: else:
raise PluginMetainfoNotFoundError( if not cls.__name__.endswith("PluginBase"):
"类`{cls_name}`必须定义一个`metainfo`属性。".format( raise PluginMetainfoNotFoundError(
cls_name=cls.__name__ "类`{cls_name}`必须定义一个`metainfo`属性。".format(
cls_name=cls.__name__
)
) )
)
class TopInOutBasePlugin(TopBasePlugin, ABC): class TopInOutPluginBase(TopPluginBase, ABC):
"""导入导出用抽象基类""" """导入导出用抽象基类"""
supported_formats: Tuple[str, ...] = tuple() supported_formats: Tuple[str, ...] = tuple()
@@ -218,15 +222,15 @@ class TopInOutBasePlugin(TopBasePlugin, ABC):
return format_name.upper().endswith(self.supported_formats) return format_name.upper().endswith(self.supported_formats)
class MusicInputPlugin(TopInOutBasePlugin, ABC): class MusicInputPluginBase(TopInOutPluginBase, ABC):
"""导入用插件抽象基类-完整曲目""" """导入用插件抽象基类-完整曲目"""
def __init_subclass__(cls) -> None: def __init_subclass__(cls) -> None:
super().__init_subclass__() super().__init_subclass__()
if cls.metainfo.type != PluginType.FUNCTION_IMPORT: if cls.metainfo.type != PluginType.FUNCTION_MUSIC_IMPORT:
raise PluginMetainfoValueError( raise PluginMetainfoValueError(
"插件类`{cls_name}`是从`MusicInputPlugin`继承的,该类的子类应当为一个`PluginType.FUNCTION_IMPORT`类型的插件,而不是`PluginType.{cls_type}`".format( "插件类`{cls_name}`是从`MusicInputPlugin`继承的,该类的子类应当为一个`PluginType.FUNCTION_MUSIC_IMPORT`类型的插件,而不是`PluginType.{cls_type}`".format(
cls_name=cls.__name__, cls_name=cls.__name__,
cls_type=cls.metainfo.type.name, cls_type=cls.metainfo.type.name,
) )
@@ -245,15 +249,15 @@ class MusicInputPlugin(TopInOutBasePlugin, ABC):
return self.loadbytes(f, config) return self.loadbytes(f, config)
class TrackInputPlugin(TopInOutBasePlugin, ABC): class TrackInputPluginBase(TopInOutPluginBase, ABC):
"""导入用插件抽象基类-单个音轨""" """导入用插件抽象基类-单个音轨"""
def __init_subclass__(cls) -> None: def __init_subclass__(cls) -> None:
super().__init_subclass__() super().__init_subclass__()
if cls.metainfo.type != PluginType.FUNCTION_IMPORT: if cls.metainfo.type != PluginType.FUNCTION_TRACK_IMPORT:
raise PluginMetainfoValueError( raise PluginMetainfoValueError(
"插件类`{cls_name}`是从`TrackInputPlugin`继承的,该类的子类应当为一个`PluginType.FUNCTION_IMPORT`类型的插件,而不是`PluginType.{cls_type}`".format( "插件类`{cls_name}`是从`TrackInputPlugin`继承的,该类的子类应当为一个`PluginType.FUNCTION_TRACK_IMPORT`类型的插件,而不是`PluginType.{cls_type}`".format(
cls_name=cls.__name__, cls_name=cls.__name__,
cls_type=cls.metainfo.type.name, cls_type=cls.metainfo.type.name,
) )
@@ -272,15 +276,15 @@ class TrackInputPlugin(TopInOutBasePlugin, ABC):
return self.loadbytes(f, config) return self.loadbytes(f, config)
class MusicOperatePlugin(TopBasePlugin, ABC): class MusicOperatePluginBase(TopPluginBase, ABC):
"""音乐处理用插件抽象基类-完整曲目""" """音乐处理用插件抽象基类-完整曲目"""
def __init_subclass__(cls) -> None: def __init_subclass__(cls) -> None:
super().__init_subclass__() super().__init_subclass__()
if cls.metainfo.type != PluginType.FUNCTION_OPERATE: if cls.metainfo.type != PluginType.FUNCTION_MUSIC_OPERATE:
raise PluginMetainfoValueError( raise PluginMetainfoValueError(
"插件类`{cls_name}`是从`MusicOperatePlugin`继承的,该类的子类应当为一个`PluginType.FUNCTION_OPERATE`类型的插件,而不是`PluginType.{cls_type}`".format( "插件类`{cls_name}`是从`MusicOperatePlugin`继承的,该类的子类应当为一个`PluginType.FUNCTION_MUSIC_OPERATE`类型的插件,而不是`PluginType.{cls_type}`".format(
cls_name=cls.__name__, cls_name=cls.__name__,
cls_type=cls.metainfo.type.name, cls_type=cls.metainfo.type.name,
) )
@@ -294,15 +298,15 @@ class MusicOperatePlugin(TopBasePlugin, ABC):
pass pass
class TrackOperatePlugin(TopBasePlugin, ABC): class TrackOperatePluginBase(TopPluginBase, ABC):
"""音乐处理用插件抽象基类-单个音轨""" """音乐处理用插件抽象基类-单个音轨"""
def __init_subclass__(cls) -> None: def __init_subclass__(cls) -> None:
super().__init_subclass__() super().__init_subclass__()
if cls.metainfo.type != PluginType.FUNCTION_OPERATE: if cls.metainfo.type != PluginType.FUNCTION_TRACK_OPERATE:
raise PluginMetainfoValueError( raise PluginMetainfoValueError(
"插件类`{cls_name}`是从`TrackOperatePlugin`继承的,该类的子类应当为一个`PluginType.FUNCTION_OPERATE`类型的插件,而不是`PluginType.{cls_type}`".format( "插件类`{cls_name}`是从`TrackOperatePlugin`继承的,该类的子类应当为一个`PluginType.FUNCTION_TRACK_OPERATE`类型的插件,而不是`PluginType.{cls_type}`".format(
cls_name=cls.__name__, cls_name=cls.__name__,
cls_type=cls.metainfo.type.name, cls_type=cls.metainfo.type.name,
) )
@@ -316,15 +320,15 @@ class TrackOperatePlugin(TopBasePlugin, ABC):
pass pass
class MusicOutputPlugin(TopInOutBasePlugin, ABC): class MusicOutputPluginBase(TopInOutPluginBase, ABC):
"""导出用插件的抽象基类-完整曲目""" """导出用插件的抽象基类-完整曲目"""
def __init_subclass__(cls) -> None: def __init_subclass__(cls) -> None:
super().__init_subclass__() super().__init_subclass__()
if cls.metainfo.type != PluginType.FUNCTION_EXPORT: if cls.metainfo.type != PluginType.FUNCTION_MUSIC_EXPORT:
raise PluginMetainfoValueError( raise PluginMetainfoValueError(
"插件类`{cls_name}`是从`MusicOutputPlugin`继承的,该类的子类应当为一个`PluginType.FUNCTION_EXPORT`类型的插件,而不是`PluginType.{cls_type}`".format( "插件类`{cls_name}`是从`MusicOutputPlugin`继承的,该类的子类应当为一个`PluginType.FUNCTION_MUSIC_EXPORT`类型的插件,而不是`PluginType.{cls_type}`".format(
cls_name=cls.__name__, cls_name=cls.__name__,
cls_type=cls.metainfo.type.name, cls_type=cls.metainfo.type.name,
) )
@@ -345,15 +349,15 @@ class MusicOutputPlugin(TopInOutBasePlugin, ABC):
pass pass
class TrackOutputPlugin(TopInOutBasePlugin, ABC): class TrackOutputPluginBase(TopInOutPluginBase, ABC):
"""导出用插件的抽象基类-单个音轨""" """导出用插件的抽象基类-单个音轨"""
def __init_subclass__(cls) -> None: def __init_subclass__(cls) -> None:
super().__init_subclass__() super().__init_subclass__()
if cls.metainfo.type != PluginType.FUNCTION_EXPORT: if cls.metainfo.type != PluginType.FUNCTION_TRACK_EXPORT:
raise PluginMetainfoValueError( raise PluginMetainfoValueError(
"插件类`{cls_name}`是从`TrackOutputPlugin`继承的,该类的子类应当为一个`PluginType.FUNCTION_EXPORT`类型的插件,而不是`PluginType.{cls_type}`".format( "插件类`{cls_name}`是从`TrackOutputPlugin`继承的,该类的子类应当为一个`PluginType.FUNCTION_TRACK_EXPORT`类型的插件,而不是`PluginType.{cls_type}`".format(
cls_name=cls.__name__, cls_name=cls.__name__,
cls_type=cls.metainfo.type.name, cls_type=cls.metainfo.type.name,
) )
@@ -374,7 +378,7 @@ class TrackOutputPlugin(TopInOutBasePlugin, ABC):
pass pass
class ServicePlugin(TopBasePlugin, ABC): class ServicePluginBase(TopPluginBase, ABC):
"""服务插件抽象基类""" """服务插件抽象基类"""
def __init_subclass__(cls) -> None: def __init_subclass__(cls) -> None:
@@ -394,7 +398,7 @@ class ServicePlugin(TopBasePlugin, ABC):
pass pass
class LibraryPlugin(TopBasePlugin, ABC): class LibraryPluginBase(TopPluginBase, ABC):
"""插件依赖库的抽象基类""" """插件依赖库的抽象基类"""
def __init_subclass__(cls) -> None: def __init_subclass__(cls) -> None:

View File

@@ -0,0 +1,59 @@
# -*- 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,
PluginType,
MusicInputPluginBase,
)
@music_input_plugin("midi_2_music_plugin")
class MidiImport2MusicPlugin(MusicInputPluginBase):
"""Midi 音乐数据导入插件"""
metainfo = PluginMetaInformation(
name="midi_2_music_plugin",
author="金羿、玉衡Alioth",
description="从 Midi 文件导入音乐数据",
version=(0, 0, 1),
type=PluginType.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: 等待制作

View File

@@ -5,8 +5,8 @@
""" """
""" """
版权所有 © 2025 金羿 & 诸葛亮与八卦阵 版权所有 © 2026 金羿 & 诸葛亮与八卦阵
Copyright © 2025 Eilles & bgArray Copyright © 2026 Eilles & bgArray
开源相关声明请见 仓库根目录下的 License.md 开源相关声明请见 仓库根目录下的 License.md
Terms & Conditions: License.md in the root directory Terms & Conditions: License.md in the root directory
@@ -44,9 +44,6 @@ MIDI_PAN = "pan"
"""Midi通道立体声场偏移""" """Midi通道立体声场偏移"""
# Midi用对照表 # Midi用对照表
MIDI_DEFAULT_VOLUME_VALUE: int = ( MIDI_DEFAULT_VOLUME_VALUE: int = (

View File

@@ -421,7 +421,7 @@ class SingleTrack(List[SingleNote]):
track_name: str track_name: str
"""轨道之名称""" """轨道之名称"""
is_enabled: bool is_enabled: bool = True
"""该音轨是否启用""" """该音轨是否启用"""
track_instrument: str track_instrument: str
@@ -453,16 +453,12 @@ class SingleTrack(List[SingleNote]):
precise_time: bool = True, precise_time: bool = True,
percussion: bool = False, percussion: bool = False,
sound_direction: SoundAtmos = SoundAtmos(), sound_direction: SoundAtmos = SoundAtmos(),
enabled: bool = True,
extra_information: Dict[str, Any] = {}, extra_information: Dict[str, Any] = {},
*args: SingleNote, *args: SingleNote,
): ):
self.track_name = name self.track_name = name
"""音轨名称""" """音轨名称"""
self.is_enabled = enabled
"""音轨启用情况"""
self.track_instrument = instrument self.track_instrument = instrument
"""乐器ID""" """乐器ID"""
@@ -492,6 +488,21 @@ class SingleTrack(List[SingleNote]):
super().__init__(*args) super().__init__(*args)
super().sort() super().sort()
def disable(self) -> None:
"""禁用音轨"""
self.is_enabled = False
def enable(self) -> None:
"""启用音轨"""
self.is_enabled = True
def toggle_able(self) -> None:
"""切换音轨的启用状态"""
self.is_enabled = not self.is_enabled
def append(self, object: SingleNote) -> None: def append(self, object: SingleNote) -> None:
""" """
添加一个音符,推荐使用 add 方法 添加一个音符,推荐使用 add 方法
@@ -728,13 +739,15 @@ class SingleMusic(List[SingleTrack]):
self, start_time: float, end_time: float = inf self, start_time: float, end_time: float = inf
) -> Generator[Iterator[SingleNote], Any, None]: ) -> Generator[Iterator[SingleNote], Any, None]:
"""获取指定时间段的各个音轨的音符数据""" """获取指定时间段的各个音轨的音符数据"""
return (track.get_notes(start_time, end_time) for track in self) return (track.get_notes(start_time, end_time) for track in self.music_tracks)
def get_tracked_minenotes( def get_tracked_minenotes(
self, start_time: float, end_time: float = inf self, start_time: float, end_time: float = inf
) -> Generator[Iterator[MineNote], Any, None]: ) -> Generator[Iterator[MineNote], Any, None]:
"""获取指定时间段的各个音轨的,供我的世界播放的音符数据类""" """获取指定时间段的各个音轨的,供我的世界播放的音符数据类"""
return (track.get_minenotes(start_time, end_time) for track in self) return (
track.get_minenotes(start_time, end_time) for track in self.music_tracks
)
def get_notes( def get_notes(
self, start_time: float, end_time: float = inf self, start_time: float, end_time: float = inf
@@ -743,7 +756,7 @@ class SingleMusic(List[SingleTrack]):
if self.track_amount == 0: if self.track_amount == 0:
return iter(()) return iter(())
return self.yield_from_tracks( return self.yield_from_tracks(
[track.get_notes(start_time, end_time) for track in self], [track.get_notes(start_time, end_time) for track in self.music_tracks],
sort_key=lambda x: x.start_time, sort_key=lambda x: x.start_time,
) )
@@ -754,7 +767,7 @@ class SingleMusic(List[SingleTrack]):
if self.track_amount == 0: if self.track_amount == 0:
return return
yield from self.yield_from_tracks( yield from self.yield_from_tracks(
[track.get_minenotes(start_time, end_time) for track in self], [track.get_minenotes(start_time, end_time) for track in self.music_tracks],
sort_key=lambda x: x.start_tick, sort_key=lambda x: x.start_tick,
) )

View File

@@ -5,8 +5,8 @@
""" """
""" """
版权所有 © 2025 金羿 & 玉衡Alioth 版权所有 © 2026 金羿 & 玉衡Alioth
Copyright © 2025 Eilles & YuhengAlioth Copyright © 2026 Eilles & YuhengAlioth
开源相关声明请见 仓库根目录下的 License.md 开源相关声明请见 仓库根目录下的 License.md
Terms & Conditions: License.md in the root directory Terms & Conditions: License.md in the root directory
@@ -56,6 +56,7 @@ class MusicreaterInnerlyError(MusicreaterBaseException):
"""内部错误(面向开发者的报错信息)""" """内部错误(面向开发者的报错信息)"""
super().__init__("内部错误 - ", *args) super().__init__("内部错误 - ", *args)
class MusicreaterOuterlyError(MusicreaterBaseException): class MusicreaterOuterlyError(MusicreaterBaseException):
"""外部错误""" """外部错误"""
@@ -64,8 +65,6 @@ class MusicreaterOuterlyError(MusicreaterBaseException):
super().__init__("外部错误 - ", *args) super().__init__("外部错误 - ", *args)
class InnerlyParameterError(MusicreaterInnerlyError): class InnerlyParameterError(MusicreaterInnerlyError):
"""内部传参错误""" """内部传参错误"""
@@ -90,6 +89,13 @@ class ParameterValueError(InnerlyParameterError, ValueError):
super().__init__("参数数值错误:", *args) super().__init__("参数数值错误:", *args)
class PluginNotSpecifiedError(InnerlyParameterError, LookupError):
"""未指定插件"""
def __init__(self, *args):
"""未指定插件"""
super().__init__("未指定插件:", *args)
class OuterlyParameterError(MusicreaterOuterlyError): class OuterlyParameterError(MusicreaterOuterlyError):
"""外部参数错误""" """外部参数错误"""
@@ -115,6 +121,12 @@ class IllegalMinimumVolumeError(OuterlyParameterError, ValueError):
super().__init__("最小播放音量超出范围:", *args) super().__init__("最小播放音量超出范围:", *args)
class FileFormatNotSupportedError(MusicreaterOuterlyError):
"""不支持的文件格式"""
def __init__(self, *args):
"""文件格式不受支持"""
super().__init__("不支持的文件格式:", *args)
class NoteBinaryDecodeError(MusicreaterOuterlyError): class NoteBinaryDecodeError(MusicreaterOuterlyError):
@@ -148,6 +160,7 @@ class NoteBinaryFileVerificationFailed(NoteBinaryDecodeError):
"""音乐存储文件与其校验值不一致""" """音乐存储文件与其校验值不一致"""
super().__init__("音乐存储文件校验失败:", *args) super().__init__("音乐存储文件校验失败:", *args)
class PluginDefineError(MusicreaterInnerlyError): class PluginDefineError(MusicreaterInnerlyError):
"""插件定义错误(内部相关)""" """插件定义错误(内部相关)"""
@@ -155,6 +168,7 @@ class PluginDefineError(MusicreaterInnerlyError):
"""插件本身存在错误""" """插件本身存在错误"""
super().__init__("插件内部错误 - ", *args) super().__init__("插件内部错误 - ", *args)
class PluginInstanceNotFoundError(PluginDefineError, LookupError): class PluginInstanceNotFoundError(PluginDefineError, LookupError):
"""插件实例未找到""" """插件实例未找到"""
@@ -162,6 +176,7 @@ class PluginInstanceNotFoundError(PluginDefineError, LookupError):
"""插件实例未找到""" """插件实例未找到"""
super().__init__("插件实例未找到:", *args) super().__init__("插件实例未找到:", *args)
class PluginAttributeNotFoundError(PluginDefineError, AttributeError): class PluginAttributeNotFoundError(PluginDefineError, AttributeError):
"""插件属性定义错误""" """插件属性定义错误"""
@@ -177,6 +192,7 @@ class PluginMetainfoError(PluginDefineError):
"""插件元信息定义错误""" """插件元信息定义错误"""
super().__init__("插件元信息定义错误 - ", *args) super().__init__("插件元信息定义错误 - ", *args)
class PluginMetainfoTypeError(PluginMetainfoError, TypeError): class PluginMetainfoTypeError(PluginMetainfoError, TypeError):
"""插件元信息定义类型错误""" """插件元信息定义类型错误"""
@@ -184,6 +200,7 @@ class PluginMetainfoTypeError(PluginMetainfoError, TypeError):
"""插件元信息定义类型错误""" """插件元信息定义类型错误"""
super().__init__("插件元信息类型错误:", *args) super().__init__("插件元信息类型错误:", *args)
class PluginMetainfoValueError(PluginMetainfoError, ValueError): class PluginMetainfoValueError(PluginMetainfoError, ValueError):
"""插件元信息定义值错误""" """插件元信息定义值错误"""
@@ -191,6 +208,7 @@ class PluginMetainfoValueError(PluginMetainfoError, ValueError):
"""插件元信息定义值错误""" """插件元信息定义值错误"""
super().__init__("插件元信息数值错误:", *args) super().__init__("插件元信息数值错误:", *args)
class PluginMetainfoNotFoundError(PluginMetainfoError, PluginAttributeNotFoundError): class PluginMetainfoNotFoundError(PluginMetainfoError, PluginAttributeNotFoundError):
"""插件元信息定义缺少错误""" """插件元信息定义缺少错误"""

View File

@@ -43,13 +43,24 @@ https://gitee.com/TriM-Organization/Musicreater/blob/master/LICENSE.md。
# Words combine! Codes unite! # Words combine! Codes unite!
# Hurry to call the programmer! Let's Go! # Hurry to call the programmer! Let's Go!
import re
from difflib import get_close_matches
from typing import Dict, Generator, List, Optional, Tuple, Union from typing import Dict, Generator, List, Optional, Tuple, Union
from pathlib import Path from pathlib import Path
from .data import SingleMusic, SingleTrack from .data import SingleMusic, SingleTrack
from ._plugin_abc import TopBasePlugin from .exceptions import FileFormatNotSupportedError, PluginNotSpecifiedError
from .plugins import __global_plugin_registry, PluginRegistry from ._plugin_abc import TopPluginBase
from .plugins import (
_global_plugin_registry,
PluginRegistry,
PluginConfig,
PluginType,
load_plugin_module,
)
class MusiCreater: class MusiCreater:
@@ -60,21 +71,239 @@ class MusiCreater:
__plugin_registry: PluginRegistry __plugin_registry: PluginRegistry
"""插件注册表实例""" """插件注册表实例"""
_plugin_cache: Dict[str, TopBasePlugin] _plugin_cache: Dict[str, TopPluginBase]
"""插件缓存字典,插件名为键、插件实例为值""" """插件缓存字典,插件名为键、插件实例为值"""
music: SingleMusic music: SingleMusic
"""当前曲目实例""" """当前曲目实例"""
def __init__(self, whole_music: SingleMusic) -> None: def __init__(self, whole_music: SingleMusic) -> None:
global __global_plugin_registry global _global_plugin_registry
self.__plugin_registry = __global_plugin_registry self.__plugin_registry = _global_plugin_registry
self._plugin_cache = {} self._plugin_cache = {}
self._cache_all_plugins()
self.music = whole_music self.music = whole_music
def import_music(self, file_path: Path, plugin_name: Optional[str] = None) -> SingleMusic: @classmethod
def import_music(
cls,
file_path: Path,
plugin_name: Optional[str] = None,
plugin_config: Optional[PluginConfig] = None,
):
__music = None
if plugin_name:
__music = _global_plugin_registry.get_music_input_plugin(plugin_name).load(
file_path, plugin_config
)
else:
for plugin in _global_plugin_registry.get_music_input_plugin_by_format(
file_path
):
if __music is not None:
raise PluginNotSpecifiedError(
"文件类型`{}`可被多个插件处理,请在导入函数的参数中指定插件名称".format(
file_path.suffix.upper()
)
)
__music = plugin.load(file_path, plugin_config)
if __music is None:
raise FileFormatNotSupportedError(
"无法找到处理`{}`类型文件的插件".format(file_path.suffix.upper())
)
return cls(whole_music=__music)
def import_track(
self,
file_path: Path,
plugin_name: Optional[str] = None,
plugin_config: Optional[PluginConfig] = None,
) -> SingleTrack:
__track = None
if plugin_name:
__track = self.get_plugin_by_name(
plugin_name
).load( # pyright: ignore[reportAttributeAccessIssue]
file_path, plugin_config
)
else:
for plugin in self.__plugin_registry.get_track_input_plugin_by_format(
file_path
):
if __track:
raise PluginNotSpecifiedError(
"文件类型`{}`可被多个插件处理,请在导入函数的参数中指定插件名称".format(
file_path.suffix.upper()
)
)
__track = plugin.load(file_path, plugin_config)
if __track:
self.music.append(__track)
return __track
raise FileFormatNotSupportedError(
"无法找到处理`{}`类型文件的插件".format(file_path.suffix.upper())
)
def export_music(
self,
file_path: Path,
plugin_name: Optional[str] = None,
plugin_config: Optional[PluginConfig] = None,
) -> None:
__plugin = None
if plugin_name:
__plugin = self.get_plugin_by_name(plugin_name)
else:
for plugin in self.__plugin_registry.get_music_output_plugin_by_format(
file_path
):
if __plugin:
raise PluginNotSpecifiedError(
"文件类型`{}`可被多个插件处理,请在导出函数的参数中指定插件名称".format(
file_path.suffix.upper()
)
)
__plugin = plugin
if __plugin:
__plugin.dump( # pyright: ignore[reportAttributeAccessIssue]
self.music, file_path, plugin_config
)
else:
raise FileFormatNotSupportedError(
"无法找到处理`{}`类型文件的插件".format(file_path.suffix.upper())
)
def export_track(
self,
track_index: int,
file_path: Path,
plugin_name: Optional[str] = None,
plugin_config: Optional[PluginConfig] = None,
) -> None:
__plugin = None
if plugin_name:
__plugin = self.get_plugin_by_name(plugin_name)
else:
for plugin in self.__plugin_registry.get_track_output_plugin_by_format(
file_path
):
if __plugin:
raise PluginNotSpecifiedError(
"文件类型`{}`可被多个插件处理,请在导出函数的参数中指定插件名称".format(
file_path.suffix.upper()
)
)
__plugin = plugin
if __plugin:
__plugin.dump( # pyright: ignore[reportAttributeAccessIssue]
self.music[track_index], file_path, plugin_config
)
else:
raise FileFormatNotSupportedError(
"无法找到处理`{}`类型文件的插件".format(file_path.suffix.upper())
)
def perform_operation_on_music(
self, plugin_name: str, plugin_config: Optional[PluginConfig] = None
):
# 这样做是为了兼容以后的*撤回/重做*功能
self.music = self.get_plugin_by_name(
plugin_name
).process( # pyright: ignore[reportAttributeAccessIssue]
self.music, plugin_config
)
def perform_operation_on_track(
self,
track_index: int,
plugin_name: str,
plugin_config: Optional[PluginConfig] = None,
):
# 这样做是为了兼容以后的*撤回/重做*功能
self.music[track_index] = self.get_plugin_by_name(
plugin_name
).process( # pyright: ignore[reportAttributeAccessIssue]
self.music[track_index], plugin_config
)
@staticmethod
def _camel_to_snake(name: str) -> str:
"""
将驼峰命名转换为蛇形命名
CyberAngel -> cyber_angel
"""
return re.sub(
"([a-z0-9])([A-Z])", r"\1_\2", re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name)
).lower()
def _parse_plugin_name(self, attr_name: str) -> Optional[str]:
"""解析属性名称为插件名称"""
# 尝试去除 _plugin 后缀
if attr_name.endswith("_plugin"):
candidate_name = attr_name[:-7] # 去除 "_plugin"
if candidate_name in self._plugin_cache:
return candidate_name
# 尝试转换为 snake_case如果插件名是驼峰式
snake_case_name = self._camel_to_snake(attr_name)
if snake_case_name != attr_name: # 避免重复转换
if snake_case_name in self._plugin_cache: # 尝试转换后的插件名
return snake_case_name
else:
return self._parse_plugin_name(snake_case_name)
return None
def _get_closest_plugin_name(self, requested_name: str) -> Optional[str]:
"""找到最接近的插件名称(用于更好的错误提示)"""
matches = get_close_matches(
requested_name, self._plugin_cache.keys(), n=1, cutoff=0.6
)
return matches[0] if matches else None
def get_plugin_by_name(self, name: str) -> TopPluginBase:
"""获取插件实例,并缓存起来,提高性能"""
if name.startswith("_"):
raise AttributeError("属性`{}`不存在,不应访问类的私有属性".format(name))
if name in self._plugin_cache:
return self._plugin_cache[name]
else:
plugin_name = self._parse_plugin_name(name)
if plugin_name:
self._plugin_cache[name] = self._plugin_cache[plugin_name]
return self._plugin_cache[name]
closest = self._get_closest_plugin_name(name)
raise AttributeError(
"插件`{}`不存在,请检查插件名称是否正确".format(name)
+ (
";或者阁下可能想要使用的是`{}`插件?".format(closest)
if closest
else ""
)
)
def __getattr__(self, name: str):
"""动态属性访问,允许直接 实例.插件名 来访问插件"""
return self.get_plugin_by_name(name)
def _cache_all_plugins(self):
"""获取所有已注册插件的名称"""
for __plugin_type, __plugins_set in self.__plugin_registry:
for __plugin in __plugins_set:
if __plugin.metainfo.name in self._plugin_cache: # 避免重复缓存
if (
__plugin.metainfo.version
<= self._plugin_cache[__plugin.metainfo.name].metainfo.version
): # 优先使用版本号最大的插件
continue
self._plugin_cache[__plugin.metainfo.name] = __plugin

View File

@@ -5,8 +5,8 @@
""" """
""" """
版权所有 © 2025 金羿 版权所有 © 2026 金羿
Copyright © 2025 Eilles Copyright © 2026 Eilles
开源相关声明请见 仓库根目录下的 License.md 开源相关声明请见 仓库根目录下的 License.md
Terms & Conditions: License.md in the root directory Terms & Conditions: License.md in the root directory
@@ -19,7 +19,18 @@ Terms & Conditions: License.md in the root directory
import importlib import importlib
from pathlib import Path from pathlib import Path
from typing import Dict, Any, Optional, List, Tuple, Union, Generator, Set from typing import (
Dict,
Any,
Optional,
List,
Tuple,
Union,
Generator,
Set,
Iterable,
Iterator,
)
from itertools import chain from itertools import chain
@@ -30,14 +41,16 @@ from ._plugin_abc import (
PluginConfig, PluginConfig,
PluginMetaInformation, PluginMetaInformation,
# 抽象基类(插件定义) # 抽象基类(插件定义)
MusicInputPlugin, MusicInputPluginBase,
TrackInputPlugin, TrackInputPluginBase,
MusicOperatePlugin, MusicOperatePluginBase,
TrackOperatePlugin, TrackOperatePluginBase,
MusicOutputPlugin, MusicOutputPluginBase,
TrackOutputPlugin, TrackOutputPluginBase,
ServicePlugin, ServicePluginBase,
LibraryPlugin, LibraryPluginBase,
# 顶层插件定义
TopPluginBase,
) )
from .exceptions import ( from .exceptions import (
PluginMetainfoNotFoundError, PluginMetainfoNotFoundError,
@@ -53,14 +66,14 @@ __all__ = [
"PluginConfig", "PluginConfig",
"PluginMetaInformation", "PluginMetaInformation",
# 抽象基类(插件定义) # 抽象基类(插件定义)
"MusicInputPlugin", "MusicInputPluginBase",
"TrackInputPlugin", "TrackInputPluginBase",
"MusicOperatePlugin", "MusicOperatePluginBase",
"TrackOperatePlugin", "TrackOperatePluginBase",
"MusicOutputPlugin", "MusicOutputPluginBase",
"TrackOutputPlugin", "TrackOutputPluginBase",
"ServicePlugin", "ServicePluginBase",
"LibraryPlugin", "LibraryPluginBase",
# 插件注册用装饰函数 # 插件注册用装饰函数
"music_input_plugin", "music_input_plugin",
"track_input_plugin", "track_input_plugin",
@@ -102,14 +115,34 @@ class PluginRegistry:
# (不用 Dict[str`plugin name`, PluginClass`] 的形式) # (不用 Dict[str`plugin name`, PluginClass`] 的形式)
# 啊,我真的很高尚 # 啊,我真的很高尚
# 你真的不会把插件注册两遍吧……对吧? # 你真的不会把插件注册两遍吧……对吧?
self._music_input_plugins: Set[MusicInputPlugin] = set()
self._track_input_plugins: Set[TrackInputPlugin] = set() # EMERGENCY TODO ================================================ CRITICAL 紧急更改
self._music_operate_plugins: Set[MusicOperatePlugin] = set() # 改成 Dict[str`plugin id`, PluginClass`]] 的形式吧
self._track_operate_plugins: Set[TrackOperatePlugin] = set() # 刚刚才想起来,这个 name 是显示名称啊!草
self._music_output_plugins: Set[MusicOutputPlugin] = set() # 现在测试情况下就将错就错吧,先把 name 当成 id 来写吧
self._track_output_plugins: Set[TrackOutputPlugin] = set() self._music_input_plugins: Set[MusicInputPluginBase] = set()
self._service_plugins: Set[ServicePlugin] = set() self._track_input_plugins: Set[TrackInputPluginBase] = set()
self._library_plugins: Set[LibraryPlugin] = set() self._music_operate_plugins: Set[MusicOperatePluginBase] = set()
self._track_operate_plugins: Set[TrackOperatePluginBase] = set()
self._music_output_plugins: Set[MusicOutputPluginBase] = set()
self._track_output_plugins: Set[TrackOutputPluginBase] = set()
self._service_plugins: Set[ServicePluginBase] = set()
self._library_plugins: Set[LibraryPluginBase] = set()
def __iter__(self) -> Iterator[Tuple[PluginType, Set[TopPluginBase]]]:
"""迭代器,返回所有插件"""
return iter(
(
(PluginType.FUNCTION_MUSIC_IMPORT, self._music_input_plugins),
(PluginType.FUNCTION_TRACK_IMPORT, self._track_input_plugins),
(PluginType.FUNCTION_MUSIC_OPERATE, self._music_operate_plugins),
(PluginType.FUNCTION_TRACK_OPERATE, self._track_operate_plugins),
(PluginType.FUNCTION_MUSIC_EXPORT, self._music_output_plugins),
(PluginType.FUNCTION_TRACK_EXPORT, self._track_output_plugins),
(PluginType.SERVICE, self._service_plugins),
(PluginType.LIBRARY, self._library_plugins),
)
) # pyright: ignore[reportReturnType]
def register_music_input_plugin(self, plugin_class: type) -> None: def register_music_input_plugin(self, plugin_class: type) -> None:
"""注册输入插件-整首曲目""" """注册输入插件-整首曲目"""
@@ -145,7 +178,7 @@ class PluginRegistry:
def get_music_input_plugin_by_format( def get_music_input_plugin_by_format(
self, filepath_or_format: Union[Path, str] self, filepath_or_format: Union[Path, str]
) -> Generator[MusicInputPlugin, None, None]: ) -> Generator[MusicInputPluginBase, None, None]:
"""通过指定输入的文件或格式,以获取对应的全曲导入用插件""" """通过指定输入的文件或格式,以获取对应的全曲导入用插件"""
if isinstance(filepath_or_format, str): if isinstance(filepath_or_format, str):
return ( return (
@@ -168,7 +201,7 @@ class PluginRegistry:
def get_track_input_plugin_by_format( def get_track_input_plugin_by_format(
self, filepath_or_format: Union[Path, str] self, filepath_or_format: Union[Path, str]
) -> Generator[TrackInputPlugin, None, None]: ) -> Generator[TrackInputPluginBase, None, None]:
"""通过指定输入的文件或格式,以获取对应的单音轨导入用插件""" """通过指定输入的文件或格式,以获取对应的单音轨导入用插件"""
if isinstance(filepath_or_format, str): if isinstance(filepath_or_format, str):
return ( return (
@@ -191,7 +224,7 @@ class PluginRegistry:
def get_music_output_plugin_by_format( def get_music_output_plugin_by_format(
self, filepath_or_format: Union[Path, str] self, filepath_or_format: Union[Path, str]
) -> Generator[MusicOutputPlugin, None, None]: ) -> Generator[MusicOutputPluginBase, None, None]:
"""通过指定输出的文件或格式,以获取对应的导出全曲用插件""" """通过指定输出的文件或格式,以获取对应的导出全曲用插件"""
if isinstance(filepath_or_format, str): if isinstance(filepath_or_format, str):
return ( return (
@@ -214,7 +247,7 @@ class PluginRegistry:
def get_track_output_plugin_by_format( def get_track_output_plugin_by_format(
self, filepath_or_format: Union[Path, str] self, filepath_or_format: Union[Path, str]
) -> Generator[TrackOutputPlugin, None, None]: ) -> Generator[TrackOutputPluginBase, None, None]:
"""通过指定输出的文件或格式,以获取对应的导出单个音轨用插件""" """通过指定输出的文件或格式,以获取对应的导出单个音轨用插件"""
if isinstance(filepath_or_format, str): if isinstance(filepath_or_format, str):
return ( return (
@@ -235,7 +268,7 @@ class PluginRegistry:
) )
) )
def get_music_input_plugin(self, plugin_name: str) -> MusicInputPlugin: def get_music_input_plugin(self, plugin_name: str) -> MusicInputPluginBase:
"""获取指定名称的全曲导入用插件,当名称重叠时,取版本号最大的""" """获取指定名称的全曲导入用插件,当名称重叠时,取版本号最大的"""
try: try:
return max( return max(
@@ -251,7 +284,7 @@ class PluginRegistry:
"未找到“用于导入曲目、名为`{}`”的插件".format(plugin_name) "未找到“用于导入曲目、名为`{}`”的插件".format(plugin_name)
) )
def get_track_input_plugin(self, plugin_name: str) -> TrackInputPlugin: def get_track_input_plugin(self, plugin_name: str) -> TrackInputPluginBase:
"""获取指定名称的单音轨导入用插件,当名称重叠时,取版本号最大的""" """获取指定名称的单音轨导入用插件,当名称重叠时,取版本号最大的"""
try: try:
return max( return max(
@@ -267,7 +300,7 @@ class PluginRegistry:
"未找到“用于导入单个音轨、名为`{}`”的插件".format(plugin_name) "未找到“用于导入单个音轨、名为`{}`”的插件".format(plugin_name)
) )
def get_music_operate_plugin(self, plugin_name: str) -> MusicOperatePlugin: def get_music_operate_plugin(self, plugin_name: str) -> MusicOperatePluginBase:
"""获取指定名称的全曲处理用插件,当名称重叠时,取版本号最大的""" """获取指定名称的全曲处理用插件,当名称重叠时,取版本号最大的"""
try: try:
return max( return max(
@@ -283,7 +316,7 @@ class PluginRegistry:
"未找到“用于处理整个曲目、名为`{}`”的插件".format(plugin_name) "未找到“用于处理整个曲目、名为`{}`”的插件".format(plugin_name)
) )
def get_track_operate_plugin(self, plugin_name: str) -> TrackOperatePlugin: def get_track_operate_plugin(self, plugin_name: str) -> TrackOperatePluginBase:
"""获取指定名称的单音轨处理用插件,当名称重叠时,取版本号最大的""" """获取指定名称的单音轨处理用插件,当名称重叠时,取版本号最大的"""
try: try:
return max( return max(
@@ -299,7 +332,7 @@ class PluginRegistry:
"未找到“用于处理单个音轨、名为`{}`”的插件".format(plugin_name) "未找到“用于处理单个音轨、名为`{}`”的插件".format(plugin_name)
) )
def get_music_output_plugin(self, plugin_name: str) -> MusicOutputPlugin: def get_music_output_plugin(self, plugin_name: str) -> MusicOutputPluginBase:
"""获取指定名称的导出全曲用插件,当名称重叠时,取版本号最大的""" """获取指定名称的导出全曲用插件,当名称重叠时,取版本号最大的"""
try: try:
return max( return max(
@@ -315,7 +348,7 @@ class PluginRegistry:
"未找到“用于导出完整曲目、名为`{}`”的插件".format(plugin_name) "未找到“用于导出完整曲目、名为`{}`”的插件".format(plugin_name)
) )
def get_track_output_plugin(self, plugin_name: str) -> TrackOutputPlugin: def get_track_output_plugin(self, plugin_name: str) -> TrackOutputPluginBase:
"""获取指定名称的导出单音轨用插件,当名称重叠时,取版本号最大的""" """获取指定名称的导出单音轨用插件,当名称重叠时,取版本号最大的"""
try: try:
return max( return max(
@@ -331,7 +364,7 @@ class PluginRegistry:
"未找到“用于导出单个音轨、名为`{}`”的插件".format(plugin_name) "未找到“用于导出单个音轨、名为`{}`”的插件".format(plugin_name)
) )
def get_service_plugin(self, plugin_name: str) -> ServicePlugin: def get_service_plugin(self, plugin_name: str) -> ServicePluginBase:
"""获取服务用插件,当名称重叠时,取版本号最大的""" """获取服务用插件,当名称重叠时,取版本号最大的"""
try: try:
return max( return max(
@@ -347,7 +380,7 @@ class PluginRegistry:
"未找到名为`{}`的服务用插件".format(plugin_name) "未找到名为`{}`的服务用插件".format(plugin_name)
) )
def get_library_plugin(self, plugin_name: str) -> LibraryPlugin: def get_library_plugin(self, plugin_name: str) -> LibraryPluginBase:
"""获取依赖库类插件,当名称重叠时,取版本号最高的""" """获取依赖库类插件,当名称重叠时,取版本号最高的"""
try: try:
return max( return max(
@@ -386,101 +419,101 @@ class PluginRegistry:
) )
__global_plugin_registry = PluginRegistry() _global_plugin_registry = PluginRegistry()
"""全局插件注册表实例""" """全局插件注册表实例"""
def music_input_plugin(metainfo: PluginMetaInformation): def music_input_plugin(plugin_id: str):
"""全曲输入用插件装饰器""" """全曲输入用插件装饰器"""
def decorator(cls): def decorator(cls):
global __global_plugin_registry global _global_plugin_registry
cls.metainfo = metainfo cls.id = plugin_id
__global_plugin_registry.register_music_input_plugin(cls) _global_plugin_registry.register_music_input_plugin(cls)
return cls return cls
return decorator return decorator
def track_input_plugin(metainfo: PluginMetaInformation): def track_input_plugin(plugin_id: str):
"""单轨输入用插件装饰器""" """单轨输入用插件装饰器"""
def decorator(cls): def decorator(cls):
global __global_plugin_registry global _global_plugin_registry
cls.metainfo = metainfo cls.id = plugin_id
__global_plugin_registry.register_track_input_plugin(cls) _global_plugin_registry.register_track_input_plugin(cls)
return cls return cls
return decorator return decorator
def music_operate_plugin(metainfo: PluginMetaInformation): def music_operate_plugin(plugin_id: str):
"""全曲处理用插件装饰器""" """全曲处理用插件装饰器"""
def decorator(cls): def decorator(cls):
global __global_plugin_registry global _global_plugin_registry
cls.metainfo = metainfo cls.id = plugin_id
__global_plugin_registry.register_music_operate_plugin(cls) _global_plugin_registry.register_music_operate_plugin(cls)
return cls return cls
return decorator return decorator
def track_operate_plugin(metainfo: PluginMetaInformation): def track_operate_plugin(plugin_id: str):
"""音轨处理插件装饰器""" """音轨处理插件装饰器"""
def decorator(cls): def decorator(cls):
global __global_plugin_registry global _global_plugin_registry
cls.metainfo = metainfo cls.id = plugin_id
__global_plugin_registry.register_track_operate_plugin(cls) _global_plugin_registry.register_track_operate_plugin(cls)
return cls return cls
return decorator return decorator
def music_output_plugin(metainfo: PluginMetaInformation): def music_output_plugin(plugin_id: str):
"""乐曲输出用插件装饰器""" """乐曲输出用插件装饰器"""
def decorator(cls): def decorator(cls):
global __global_plugin_registry global _global_plugin_registry
cls.metainfo = metainfo cls.id = plugin_id
__global_plugin_registry.register_music_output_plugin(cls) _global_plugin_registry.register_music_output_plugin(cls)
return cls return cls
return decorator return decorator
def track_output_plugin(metainfo: PluginMetaInformation): def track_output_plugin(plugin_id: str):
"""音轨输出用插件装饰器""" """音轨输出用插件装饰器"""
def decorator(cls): def decorator(cls):
global __global_plugin_registry global _global_plugin_registry
cls.metainfo = metainfo cls.id = plugin_id
__global_plugin_registry.register_track_output_plugin(cls) _global_plugin_registry.register_track_output_plugin(cls)
return cls return cls
return decorator return decorator
def service_plugin(metainfo: PluginMetaInformation): def service_plugin(plugin_id: str):
"""服务插件装饰器""" """服务插件装饰器"""
def decorator(cls): def decorator(cls):
global __global_plugin_registry global _global_plugin_registry
cls.metainfo = metainfo cls.id = plugin_id
__global_plugin_registry.register_service_plugin(cls) _global_plugin_registry.register_service_plugin(cls)
return cls return cls
return decorator return decorator
def library_plugin(metainfo: PluginMetaInformation): def library_plugin(plugin_id: str):
"""支持库插件装饰器""" """支持库插件装饰器"""
def decorator(cls): def decorator(cls):
global __global_plugin_registry global _global_plugin_registry
cls.metainfo = metainfo cls.id = plugin_id
__global_plugin_registry.register_library_plugin(cls) _global_plugin_registry.register_library_plugin(cls)
return cls return cls
return decorator return decorator

View File

@@ -5,8 +5,8 @@
""" """
""" """
版权所有 © 2025 金羿 & 玉衡Alioth 版权所有 © 2026 金羿 & 玉衡Alioth
Copyright © 2025 Eilles & YuhengAlioth Copyright © 2026 Eilles & YuhengAlioth
开源相关声明请见 仓库根目录下的 License.md 开源相关声明请见 仓库根目录下的 License.md
Terms & Conditions: License.md in the root directory Terms & Conditions: License.md in the root directory
@@ -22,4 +22,4 @@ from typing import Callable, Dict, List, Literal, Mapping, Tuple, Union
FittingFunctionType = Callable[[float], float] FittingFunctionType = Callable[[float], float]
""" """
拟合函数类型 拟合函数类型
""" """

View File

@@ -63,7 +63,7 @@
pip install --upgrade -i https://pypi.python.org/simple Musicreater pip install --upgrade -i https://pypi.python.org/simple Musicreater
``` ```
- 克隆仓库并安装最新版本**不推荐** - 克隆仓库并安装最新内容**不推荐**
```bash ```bash
git clone https://gitee.com/TriM-Organization/Musicreater.git git clone https://gitee.com/TriM-Organization/Musicreater.git

View File

@@ -35,7 +35,7 @@
[简体中文 🇨🇳](README.md) | English🇬🇧 [简体中文 🇨🇳](README.md) | English🇬🇧
**Notice that the localizations of documents may NOT be up-to-date.** **Notice that the localizations of documents may probably NOT be up-to-date. The original document is in Chinese.**
## Introduction🚀 ## Introduction🚀

40
TO-DO.md Normal file
View File

@@ -0,0 +1,40 @@
# 任务清单
## 待办事项
- 乐曲文件格式设计
目前想到的是
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. 对于听感优化的实验性功能插值偏移
- 测试参数曲线的功能
- 支持导出音符盒构成的音乐
- 支持导出成 schematic 结构
## 讨论
1. 是否应该在插件注册表 PluginRegistry 中采用 `Dict[插件名, 插件对象]` 的形式存储插件
当前不采用这种方式是认为可以兼容一些极端场景下用户将一堆同名插件放在一起的情况但是就算是插件放在一起我们也可以有选择地读入注册表比如依照版本号只读取最高版本的插件并不需要全部存储在插件注册表中所以其实用字典来存储是有利的
2. 服务插件到底该怎么写总不能留着一个 PluginType.SERVICE 的插件一直空在那里吧

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1 @@
q@v,fxіБ<D196>Еџ<D095>лцmЩ5]Ќs"ЏџЦбBMXi<58>ЈnНхч<D185>Z8О=Г<7F>4<EFBFBD>PTUБQЈфджG<D0B6>жu_цп<D186>DS№|

View File

@@ -0,0 +1,6 @@
MSCT_MAIN:
6b9f5a97d50beb07c834e375104c67ae44c57ae40f73fb71075b3668899029c7
MSCT_PLUGIN:
c280413a394a539438a5d10078c9b55f04bcd4cf6869c59a3f7a026039748cfc
MSCT_PLUGIN_FUNCTION:
40697f1d9b293268fe142fa3e9bffee2923a8f4811ec7bbdf7b14afb98723ef2

View File

View File

@@ -0,0 +1,67 @@
import mido
import numpy
'''
bpm
bites per minutes
每分钟的拍数
'''
def mt2gt(mt, tpb_a, bpm_a):
return round(mt / tpb_a / bpm_a * 60)
def get(mid:mido.MidiFile) -> int:
'''传入一个 MidiFile, 返回其音乐的bpm
:param mid : mido.MidFile
mido库识别的midi文件数据
:return bpm : int
'''
# mid = mido.MidiFile(mf)
length = mid.length
tpb = mid.ticks_per_beat
bpm = 20
gotV = 0
for track in mid.tracks:
global_time = 0
for msg in track:
global_time += msg.time
if msg.type == "note_on" and msg.velocity > 0:
gotV = mt2gt(global_time, tpb, bpm)
errorV = numpy.fabs(gotV - length)
last_dic = {bpm: errorV}
if last_dic.get(bpm) > errorV:
last_dic = {bpm: errorV}
bpm += 2
while True:
for track in mid.tracks:
global_time = 0
for msg in track:
global_time += msg.time
if msg.type == "note_on" and msg.velocity > 0:
gotV = mt2gt(global_time, tpb, bpm)
errorV = numpy.fabs(gotV - length)
try:
if last_dic.get(bpm - 2) > errorV:
last_dic = {bpm: errorV}
except TypeError:
pass
bpm += 2
if bpm >= 252:
break
print(list(last_dic.keys())[0])
return list(last_dic.keys())[0]
def compute(mid:mido.MidiFile):
answer = 60000000/mid.ticks_per_beat
print(answer)
return answer
if __name__ == '__main__':
mid = mido.MidiFile(r"C:\Users\lc\Documents\MuseScore3\乐谱\乐谱\Bad style - Time back.mid")
get(mid)
compute(mid)

View File

@@ -0,0 +1,40 @@
def round_up(num, power=0):
"""
实现精确四舍五入,包含正、负小数多种场景
:param num: 需要四舍五入的小数
:param power: 四舍五入位数支持0-∞
:return: 返回四舍五入后的结果
"""
try:
print(1 / 0)
except ZeroDivisionError:
digit = 10 ** power
num2 = float(int(num * digit))
# 处理正数power不为0的情况
if num >= 0 and power != 0:
tag = num * digit - num2 + 1 / (digit * 10)
if tag >= 0.5:
return (num2 + 1) / digit
else:
return num2 / digit
# 处理正数power为0取整的情况
elif num >= 0 and power == 0:
tag = num * digit - int(num)
if tag >= 0.5:
return (num2 + 1) / digit
else:
return num2 / digit
# 处理负数power为0取整的情况
elif power == 0 and num < 0:
tag = num * digit - int(num)
if tag <= -0.5:
return (num2 - 1) / digit
else:
return num2 / digit
# 处理负数power不为0的情况
else:
tag = num * digit - num2 - 1 / (digit * 10)
if tag <= -0.5:
return (num2 - 1) / digit
else:
return num2 / digit

View File

@@ -0,0 +1,130 @@
instrument_list = {
"0": "harp",
"1": "harp",
"2": "pling",
"3": "harp",
"4": "pling",
"5": "pling",
"6": "harp",
"7": "harp",
"8": "share",
"9": "harp",
"10": "didgeridoo",
"11": "harp",
"12": "xylophone",
"13": "chime",
"14": "harp",
"15": "harp",
"16": "bass",
"17": "harp",
"18": "harp",
"19": "harp",
"20": "harp",
"21": "harp",
"22": "harp",
"23": "guitar",
"24": "guitar",
"25": "guitar",
"26": "guitar",
"27": "guitar",
"28": "guitar",
"29": "guitar",
"30": "guitar",
"31": "bass",
"32": "bass",
"33": "bass",
"34": "bass",
"35": "bass",
"36": "bass",
"37": "bass",
"38": "bass",
"39": "bass",
"40": "harp",
"41": "harp",
"42": "harp",
"43": "harp",
"44": "iron_xylophone",
"45": "guitar",
"46": "harp",
"47": "harp",
"48": "guitar",
"49": "guitar",
"50": "bit",
"51": "bit",
"52": "harp",
"53": "harp",
"54": "bit",
"55": "flute",
"56": "flute",
"57": "flute",
"58": "flute",
"59": "flute",
"60": "flute",
"61": "flute",
"62": "flute",
"63": "flute",
"64": "bit",
"65": "bit",
"66": "bit",
"67": "bit",
"68": "flute",
"69": "harp",
"70": "harp",
"71": "flute",
"72": "flute",
"73": "flute",
"74": "harp",
"75": "flute",
"76": "harp",
"77": "harp",
"78": "harp",
"79": "harp",
"80": "bit",
"81": "bit",
"82": "bit",
"83": "bit",
"84": "bit",
"85": "bit",
"86": "bit",
"87": "bit",
"88": "bit",
"89": "bit",
"90": "bit",
"91": "bit",
"92": "bit",
"93": "bit",
"94": "bit",
"95": "bit",
"96": "bit",
"97": "bit",
"98": "bit",
"99": "bit",
"100": "bit",
"101": "bit",
"102": "bit",
"103": "bit",
"104": "harp",
"105": "banjo",
"106": "harp",
"107": "harp",
"108": "harp",
"109": "harp",
"110": "harp",
"111": "guitar",
"112": "harp",
"113": "bell",
"114": "harp",
"115": "cow_bell",
"116": "basedrum",
"117": "bass",
"118": "bit",
"119": "basedrum",
"120": "guitar",
"121": "harp",
"122": "harp",
"123": "harp",
"124": "harp",
"125": "hat",
"126": "basedrum",
"127": "snare",
}

View File

@@ -0,0 +1,250 @@
zip_name = {
-1: "-1.Acoustic_Kit_打击乐.zip",
0: "0.Acoustic_Grand_Piano_大钢琴.zip",
1: "1.Bright_Acoustic_Piano_亮音大钢琴.zip",
10: "10.Music_Box_八音盒.zip",
100: "100.FX_brightness_合成特效-亮音.zip",
101: "101.FX_goblins_合成特效-小妖.zip",
102: "102.FX_echoes_合成特效-回声.zip",
103: "103.FX_sci-fi_合成特效-科幻.zip",
104: "104.Sitar_锡塔尔.zip",
105: "105.Banjo_班卓.zip",
106: "106.Shamisen_三味线.zip",
107: "107.Koto_筝.zip",
108: "108.Kalimba_卡林巴.zip",
109: "109.Bagpipe_风笛.zip",
11: "11.Vibraphone_电颤琴.zip",
110: "110.Fiddle_古提琴.zip",
111: "111.Shanai_唢呐.zip",
112: "112.Tinkle_Bell_铃铛.zip",
113: "113.Agogo_拉丁打铃.zip",
114: "114.Steel_Drums_钢鼓.zip",
115: "115.Woodblock_木块.zip",
116: "116.Taiko_Drum_太鼓.zip",
117: "117.Melodic_Tom_嗵鼓.zip",
118: "118.Synth_Drum_合成鼓.zip",
119: "119.Reverse_Cymbal_镲波形反转.zip",
12: "12.Marimba_马林巴.zip",
13: "13.Xylophone_木琴.zip",
14: "14.Tubular_Bells_管钟.zip",
15: "15.Dulcimer_扬琴.zip",
16: "16.Drawbar_Organ_击杆风琴.zip",
17: "17.Percussive_Organ_打击型风琴.zip",
18: "18.Rock_Organ_摇滚风琴.zip",
19: "19.Church_Organ_管风琴.zip",
2: "2.Electric_Grand_Piano_电子大钢琴.zip",
20: "20.Reed_Organ_簧风琴.zip",
21: "21.Accordion_手风琴.zip",
22: "22.Harmonica_口琴.zip",
23: "23.Tango_Accordian_探戈手风琴.zip",
24: "24.Acoustic_Guitar_(nylon)_尼龙弦吉他.zip",
25: "25.Acoustic_Guitar(steel)_钢弦吉他.zip",
26: "26.Electric_Guitar_(jazz)_爵士乐电吉他.zip",
27: "27.Electric_Guitar_(clean)_清音电吉他.zip",
28: "28.Electric_Guitar_(muted)_弱音电吉他.zip",
29: "29.Overdriven_Guitar_驱动音效吉他.zip",
3: "3.Honky-Tonk_Piano_酒吧钢琴.zip",
30: "30.Distortion_Guitar_失真音效吉他.zip",
31: "31.Guitar_Harmonics_吉他泛音.zip",
32: "32.Acoustic_Bass_原声贝司.zip",
33: "33.Electric_Bass(finger)_指拨电贝司.zip",
34: "34.Electric_Bass(pick)_拨片拨电贝司.zip",
35: "35.Fretless_Bass_无品贝司.zip",
36: "36.Slap_Bass_A_击弦贝司A.zip",
37: "37.Slap_Bass_B_击弦贝司B.zip",
38: "38.Synth_Bass_A_合成贝司A.zip",
39: "39.Synth_Bass_B_合成贝司B.zip",
4: "4.Electric_Piano_1_电钢琴A.zip",
40: "40.Violin_小提琴.zip",
41: "41.Viola_中提琴.zip",
42: "42.Cello_大提琴.zip",
43: "43.Contrabass_低音提琴.zip",
44: "44.Tremolo_Strings_弦乐震音.zip",
45: "45.Pizzicato_Strings_弦乐拨奏.zip",
46: "46.Orchestral_Harp_竖琴.zip",
47: "47.Timpani_定音鼓.zip",
48: "48.String_Ensemble_A_弦乐合奏A.zip",
49: "49.String_Ensemble_B_弦乐合奏B.zip",
5: "5.Electric_Piano_2_电钢琴B.zip",
50: "50.SynthStrings_A_合成弦乐A.zip",
51: "51.SynthStrings_B_合成弦乐B.zip",
52: "52.Choir_Aahs_合唱“啊”音.zip",
53: "53.Voice_Oohs_人声“哦”音.zip",
54: "54.Synth_Voice_合成人声.zip",
55: "55.Orchestra_Hit_乐队打击乐.zip",
56: "56.Trumpet_小号.zip",
57: "57.Trombone_长号.zip",
58: "58.Tuba_大号.zip",
59: "59.Muted_Trumpet_弱音小号.zip",
6: "6.Harpsichord_拨弦古钢琴.zip",
60: "60.French_Horn_圆号.zip",
61: "61.Brass_Section_铜管组.zip",
62: "62.Synth_Brass_A_合成铜管A.zip",
63: "63.Synth_Brass_A_合成铜管B.zip",
64: "64.Soprano_Sax_高音萨克斯.zip",
65: "65.Alto_Sax_中音萨克斯.zip",
66: "66.Tenor_Sax_次中音萨克斯.zip",
67: "67.Baritone_Sax_上低音萨克斯.zip",
68: "68.Oboe_双簧管.zip",
69: "69.English_Horn_英国管.zip",
7: "7.Clavinet_击弦古钢琴.zip",
70: "70.Bassoon_大管.zip",
71: "71.Clarinet_单簧管.zip",
72: "72.Piccolo_短笛.zip",
73: "73.Flute_长笛.zip",
74: "74.Recorder_竖笛.zip",
75: "75.Pan_Flute_排笛.zip",
76: "76.Bottle_Blow_吹瓶口.zip",
77: "77.Skakuhachi_尺八.zip",
78: "78.Whistle_哨.zip",
79: "79.Ocarina_洋埙.zip",
8: "8.Celesta_钢片琴.zip",
80: "80.Lead_square_合成主音-方波.zip",
81: "81.Lead_sawtooth_合成主音-锯齿波.zip",
82: "82.Lead_calliope_lead_合成主音-汽笛风琴.zip",
83: "83.Lead_chiff_lead_合成主音-吹管.zip",
84: "84.Lead_charang_合成主音5-吉他.zip",
85: "85.Lead_voice_合成主音-人声.zip",
86: "86.Lead_fifths_合成主音-五度.zip",
87: "87.Lead_bass+lead_合成主音-低音加主音.zip",
88: "88.Pad_new_age_合成柔音-新时代.zip",
89: "89.Pad_warm_合成柔音-暖音.zip",
9: "9.Glockenspiel_钟琴.zip",
90: "90.Pad_polysynth_合成柔音-复合成.zip",
91: "91.Pad_choir_合成柔音-合唱.zip",
92: "92.Pad_bowed_合成柔音-弓弦.zip",
93: "93.Pad_metallic_合成柔音-金属.zip",
94: "94.Pad_halo_合成柔音-光环.zip",
95: "95.Pad_sweep_合成柔音-扫弦.zip",
96: "96.FX_rain_合成特效-雨.zip",
97: "97.FX_soundtrack_合成特效-音轨.zip",
98: "98.FX_crystal_合成特效-水晶.zip",
99: "99.FX_atmosphere_合成特效-大气.zip",
}
mcpack_name = {
-1: "-1.Acoustic_Kit_打击乐.mcpack",
0: "0.Acoustic_Grand_Piano_大钢琴.mcpack",
1: "1.Bright_Acoustic_Piano_亮音大钢琴.mcpack",
10: "10.Music_Box_八音盒.mcpack",
100: "100.FX_brightness_合成特效-亮音.mcpack",
101: "101.FX_goblins_合成特效-小妖.mcpack",
102: "102.FX_echoes_合成特效-回声.mcpack",
103: "103.FX_sci-fi_合成特效-科幻.mcpack",
104: "104.Sitar_锡塔尔.mcpack",
105: "105.Banjo_班卓.mcpack",
106: "106.Shamisen_三味线.mcpack",
107: "107.Koto_筝.mcpack",
108: "108.Kalimba_卡林巴.mcpack",
109: "109.Bagpipe_风笛.mcpack",
11: "11.Vibraphone_电颤琴.mcpack",
110: "110.Fiddle_古提琴.mcpack",
111: "111.Shanai_唢呐.mcpack",
112: "112.Tinkle_Bell_铃铛.mcpack",
113: "113.Agogo_拉丁打铃.mcpack",
114: "114.Steel_Drums_钢鼓.mcpack",
115: "115.Woodblock_木块.mcpack",
116: "116.Taiko_Drum_太鼓.mcpack",
117: "117.Melodic_Tom_嗵鼓.mcpack",
118: "118.Synth_Drum_合成鼓.mcpack",
119: "119.Reverse_Cymbal_镲波形反转.mcpack",
12: "12.Marimba_马林巴.mcpack",
13: "13.Xylophone_木琴.mcpack",
14: "14.Tubular_Bells_管钟.mcpack",
15: "15.Dulcimer_扬琴.mcpack",
16: "16.Drawbar_Organ_击杆风琴.mcpack",
17: "17.Percussive_Organ_打击型风琴.mcpack",
18: "18.Rock_Organ_摇滚风琴.mcpack",
19: "19.Church_Organ_管风琴.mcpack",
2: "2.Electric_Grand_Piano_电子大钢琴.mcpack",
20: "20.Reed_Organ_簧风琴.mcpack",
21: "21.Accordion_手风琴.mcpack",
22: "22.Harmonica_口琴.mcpack",
23: "23.Tango_Accordian_探戈手风琴.mcpack",
24: "24.Acoustic_Guitar_(nylon)_尼龙弦吉他.mcpack",
25: "25.Acoustic_Guitar(steel)_钢弦吉他.mcpack",
26: "26.Electric_Guitar_(jazz)_爵士乐电吉他.mcpack",
27: "27.Electric_Guitar_(clean)_清音电吉他.mcpack",
28: "28.Electric_Guitar_(muted)_弱音电吉他.mcpack",
29: "29.Overdriven_Guitar_驱动音效吉他.mcpack",
3: "3.Honky-Tonk_Piano_酒吧钢琴.mcpack",
30: "30.Distortion_Guitar_失真音效吉他.mcpack",
31: "31.Guitar_Harmonics_吉他泛音.mcpack",
32: "32.Acoustic_Bass_原声贝司.mcpack",
33: "33.Electric_Bass(finger)_指拨电贝司.mcpack",
34: "34.Electric_Bass(pick)_拨片拨电贝司.mcpack",
35: "35.Fretless_Bass_无品贝司.mcpack",
36: "36.Slap_Bass_A_击弦贝司A.mcpack",
37: "37.Slap_Bass_B_击弦贝司B.mcpack",
38: "38.Synth_Bass_A_合成贝司A.mcpack",
39: "39.Synth_Bass_B_合成贝司B.mcpack",
4: "4.Electric_Piano_1_电钢琴A.mcpack",
40: "40.Violin_小提琴.mcpack",
41: "41.Viola_中提琴.mcpack",
42: "42.Cello_大提琴.mcpack",
43: "43.Contrabass_低音提琴.mcpack",
44: "44.Tremolo_Strings_弦乐震音.mcpack",
45: "45.Pizzicato_Strings_弦乐拨奏.mcpack",
46: "46.Orchestral_Harp_竖琴.mcpack",
47: "47.Timpani_定音鼓.mcpack",
48: "48.String_Ensemble_A_弦乐合奏A.mcpack",
49: "49.String_Ensemble_B_弦乐合奏B.mcpack",
5: "5.Electric_Piano_2_电钢琴B.mcpack",
50: "50.SynthStrings_A_合成弦乐A.mcpack",
51: "51.SynthStrings_B_合成弦乐B.mcpack",
52: "52.Choir_Aahs_合唱“啊”音.mcpack",
53: "53.Voice_Oohs_人声“哦”音.mcpack",
54: "54.Synth_Voice_合成人声.mcpack",
55: "55.Orchestra_Hit_乐队打击乐.mcpack",
56: "56.Trumpet_小号.mcpack",
57: "57.Trombone_长号.mcpack",
58: "58.Tuba_大号.mcpack",
59: "59.Muted_Trumpet_弱音小号.mcpack",
6: "6.Harpsichord_拨弦古钢琴.mcpack",
60: "60.French_Horn_圆号.mcpack",
61: "61.Brass_Section_铜管组.mcpack",
62: "62.Synth_Brass_A_合成铜管A.mcpack",
63: "63.Synth_Brass_A_合成铜管B.mcpack",
64: "64.Soprano_Sax_高音萨克斯.mcpack",
65: "65.Alto_Sax_中音萨克斯.mcpack",
66: "66.Tenor_Sax_次中音萨克斯.mcpack",
67: "67.Baritone_Sax_上低音萨克斯.mcpack",
68: "68.Oboe_双簧管.mcpack",
69: "69.English_Horn_英国管.mcpack",
7: "7.Clavinet_击弦古钢琴.mcpack",
70: "70.Bassoon_大管.mcpack",
71: "71.Clarinet_单簧管.mcpack",
72: "72.Piccolo_短笛.mcpack",
73: "73.Flute_长笛.mcpack",
74: "74.Recorder_竖笛.mcpack",
75: "75.Pan_Flute_排笛.mcpack",
76: "76.Bottle_Blow_吹瓶口.mcpack",
77: "77.Skakuhachi_尺八.mcpack",
78: "78.Whistle_哨.mcpack",
79: "79.Ocarina_洋埙.mcpack",
8: "8.Celesta_钢片琴.mcpack",
80: "80.Lead_square_合成主音-方波.mcpack",
81: "81.Lead_sawtooth_合成主音-锯齿波.mcpack",
82: "82.Lead_calliope_lead_合成主音-汽笛风琴.mcpack",
83: "83.Lead_chiff_lead_合成主音-吹管.mcpack",
84: "84.Lead_charang_合成主音5-吉他.mcpack",
85: "85.Lead_voice_合成主音-人声.mcpack",
86: "86.Lead_fifths_合成主音-五度.mcpack",
87: "87.Lead_bass+lead_合成主音-低音加主音.mcpack",
88: "88.Pad_new_age_合成柔音-新时代.mcpack",
89: "89.Pad_warm_合成柔音-暖音.mcpack",
9: "9.Glockenspiel_钟琴.mcpack",
90: "90.Pad_polysynth_合成柔音-复合成.mcpack",
91: "91.Pad_choir_合成柔音-合唱.mcpack",
92: "92.Pad_bowed_合成柔音-弓弦.mcpack",
93: "93.Pad_metallic_合成柔音-金属.mcpack",
94: "94.Pad_halo_合成柔音-光环.mcpack",
95: "95.Pad_sweep_合成柔音-扫弦.mcpack",
96: "96.FX_rain_合成特效-雨.mcpack",
97: "97.FX_soundtrack_合成特效-音轨.mcpack",
98: "98.FX_crystal_合成特效-水晶.mcpack",
99: "99.FX_atmosphere_合成特效-大气.mcpack",
}
if __name__ == "__main__":
print(zip_name[0])

View File

@@ -0,0 +1,134 @@
pitch = {
"0": "0.0220970869120796",
"1": "0.0234110480761981",
"2": "0.0248031414370031",
"3": "0.0262780129766786",
"4": "0.0278405849418856",
"5": "0.0294960722713029",
"6": "0.03125",
"7": "0.033108221698728",
"8": "0.0350769390096679",
"9": "0.037162722343835",
"10": "0.0393725328092148",
"11": "0.0417137454428136",
"12": "0.0441941738241592",
"13": "0.0468220961523963",
"14": "0.0496062828740062",
"15": "0.0525560259533572",
"16": "0.0556811698837712",
"17": "0.0589921445426059",
"18": "0.0625",
"19": "0.066216443397456",
"20": "0.0701538780193358",
"21": "0.0743254446876701",
"22": "0.0787450656184296",
"23": "0.0834274908856271",
"24": "0.0883883476483184",
"25": "0.0936441923047926",
"26": "0.0992125657480125",
"27": "0.105112051906714",
"28": "0.111362339767542",
"29": "0.117984289085212",
"30": "0.125",
"31": "0.132432886794912",
"32": "0.140307756038672",
"33": "0.14865088937534",
"34": "0.157490131236859",
"35": "0.166854981771254",
"36": "0.176776695296637",
"37": "0.187288384609585",
"38": "0.198425131496025",
"39": "0.210224103813429",
"40": "0.222724679535085",
"41": "0.235968578170423",
"42": "0.25",
"43": "0.264865773589824",
"44": "0.280615512077343",
"45": "0.29730177875068",
"46": "0.314980262473718",
"47": "0.333709963542509",
"48": "0.353553390593274",
"49": "0.37457676921917",
"50": "0.39685026299205",
"51": "0.420448207626857",
"52": "0.44544935907017",
"53": "0.471937156340847",
"54": "0.5",
"55": "0.529731547179648",
"56": "0.561231024154687",
"57": "0.594603557501361",
"58": "0.629960524947437",
"59": "0.667419927085017",
"60": "0.707106781186548",
"61": "0.749153538438341",
"62": "0.7937005259841",
"63": "0.840896415253715",
"64": "0.890898718140339",
"65": "0.943874312681694",
"66": "1",
"67": "1.0594630943593",
"68": "1.12246204830937",
"69": "1.18920711500272",
"70": "1.25992104989487",
"71": "1.33483985417003",
"72": "1.4142135623731",
"73": "1.49830707687668",
"74": "1.5874010519682",
"75": "1.68179283050743",
"76": "1.78179743628068",
"77": "1.88774862536339",
"78": "2",
"79": "2.11892618871859",
"80": "2.24492409661875",
"81": "2.37841423000544",
"82": "2.51984209978975",
"83": "2.66967970834007",
"84": "2.82842712474619",
"85": "2.99661415375336",
"86": "3.1748021039364",
"87": "3.36358566101486",
"88": "3.56359487256136",
"89": "3.77549725072677",
"90": "4",
"91": "4.23785237743718",
"92": "4.48984819323749",
"93": "4.75682846001088",
"94": "5.03968419957949",
"95": "5.33935941668014",
"96": "5.65685424949238",
"97": "5.99322830750673",
"98": "6.3496042078728",
"99": "6.72717132202972",
"100": "7.12718974512272",
"101": "7.55099450145355",
"102": "8",
"103": "8.47570475487436",
"104": "8.97969638647498",
"105": "9.51365692002177",
"106": "10.079368399159",
"107": "10.6787188333603",
"108": "11.3137084989848",
"109": "11.9864566150135",
"110": "12.6992084157456",
"111": "13.4543426440594",
"112": "14.2543794902454",
"113": "15.1019890029071",
"114": "16",
"115": "16.9514095097487",
"116": "17.95939277295",
"117": "19.0273138400435",
"118": "20.158736798318",
"119": "21.3574376667206",
"120": "22.6274169979695",
"121": "23.9729132300269",
"122": "25.3984168314912",
"123": "26.9086852881189",
"124": "28.5087589804909",
"125": "30.2039780058142",
"126": "32",
"127": "33.9028190194974",
"128": "35.9187855458999",
"129": "38.0546276800871",
"130": "40.3174735966359",
"131": "42.7148753334411",
}

View File

@@ -0,0 +1,208 @@
# -*- coding: utf-8 -*-
# from nmcsup.log import log
import pickle
class Note:
def __init__(self, channel, pitch, velocity, time, time_position, instrument):
self.channel = channel
self.pitch = pitch
self.velocity = velocity
self.delay = time
self.time_position = time_position
self.instrument = instrument
self.CD = "d"
def get_CD(self, start, end):
if end - start > 1.00:
self.CD = "c"
else:
self.CD = "d"
def midiNewReader(midfile: str):
import mido
# from msctspt.threadOpera import NewThread
from bgArrayLib.bpm import get
def Time(mt, tpb_a, bpm_a):
return round(mt / tpb_a / bpm_a * 60 * 20)
Notes = []
tracks = []
note_list = []
close = []
on = []
off = []
instruments = []
isPercussion = False
try:
mid = mido.MidiFile(midfile)
except Exception:
print("找不到文件或无法读取文件" + midfile)
return False
tpb = mid.ticks_per_beat
bpm = get(mid)
# 解析
# def loadMidi(track1):
for track in mid.tracks:
overallTime = 0.0
instrument = 0
for i in track:
overallTime += i.time
try:
if i.channel != 9:
# try:
# log("event_type(事件): " + str(i.type) + " channel(音轨): " + str(i.channel) +
# " note/pitch(音高): " +
# str(i[2]) +
# " velocity(力度): " + str(i.velocity) + " time(间隔时间): " + str(i.time) +
# " overallTime/globalTime/timePosition: " + str(overallTime) + " \n")
# except AttributeError:
# log("event_type(事件): " + str(i.type) + " thing(内容)" + str(i) + " \n")
if "program_change" in str(i):
instrument = i.program
if instrument > 119: # 音色不够
pass
else:
instruments.append(i.program)
if "note_on" in str(i) and i.velocity > 0:
print(i)
# print(i.note)
# print([Note(i.channel, i.note, i.velocity, i.time, Time(overallTime, tpb, bpm), instrument)])
tracks.append(
[
Note(
i.channel,
i.note,
i.velocity,
i.time,
Time(overallTime, tpb, bpm),
instrument,
)
]
)
note_list.append(
[
i.channel,
i.note,
i.velocity,
i.time,
Time(overallTime, tpb, bpm),
instrument,
]
)
on.append([i.note, Time(overallTime, tpb, bpm)])
# return [Note(i.channel, i, i.velocity, i.time, Time(overallTime, tpb, bpm))]
if "note_off" in str(i) or "note_on" in str(i) and i.velocity == 0:
# print(i)
# print([Note(i.channel, i.note, i.velocity, i.time, Time(overallTime, tpb, bpm))])
close.append(
[
Note(
i.channel,
i.note,
i.velocity,
i.time,
Time(overallTime, tpb, bpm),
instrument,
)
]
)
off.append([i.note, Time(overallTime, tpb, bpm)])
# return [Note(i.channel, i, i.velocity, i.time, Time(overallTime, tpb, bpm))]
except AttributeError:
pass
if "note_on" in str(i) and i.channel == 9:
if "note_on" in str(i) and i.velocity > 0:
print(i)
# print(i.note)
# print([Note(i.channel, i.note, i.velocity, i.time, Time(overallTime, tpb, bpm), -1)])
tracks.append(
[
Note(
i.channel,
i.note,
i.velocity,
i.time,
Time(overallTime, tpb, bpm),
-1,
)
]
)
note_list.append(
[
i.channel,
i.note,
i.velocity,
i.time,
Time(overallTime, tpb, bpm),
-1,
]
)
on.append([i.note, Time(overallTime, tpb, bpm)])
isPercussion = True
# return [Note(i.channel, i, i.velocity, i.time, Time(overallTime, tpb, bpm))]
Notes.append(tracks)
if instruments is []:
instruments.append(0)
instruments = list(set(instruments))
with open("1.pkl", "wb") as b:
pickle.dump([instruments, isPercussion], b)
# for j, track in enumerate(mid.tracks):
# th = NewThread(loadMidi, (track,))
# th.start()
# Notes.append(th.getResult())
# print(Notes)
print(Notes.__len__())
# print(note_list)
print(instruments)
return Notes
# return [Notes, note_list]
def midiClassReader(midfile: str):
import mido
from bgArrayLib.bpm import get
def Time(mt, tpb_a, bpm_a):
return round(mt / tpb_a / bpm_a * 60 * 20)
Notes = []
tracks = []
try:
mid = mido.MidiFile(filename=midfile, clip=True)
except Exception:
print("找不到文件或无法读取文件" + midfile)
return False
print("midi已经载入了。")
tpb = mid.ticks_per_beat
bpm = get(mid)
for track in mid.tracks:
overallTime = 0.0
instrument = 0
for i in track:
overallTime += i.time
if "note_on" in str(i) and i.velocity > 0:
print(i)
tracks.append(
[
Note(
i.channel,
i.note,
i.velocity,
i.time,
Time(overallTime, tpb, bpm),
instrument,
)
]
)
Notes.append(tracks)
print(Notes.__len__())
return Notes

View File

@@ -0,0 +1,147 @@
import os
import pickle
import shutil
# import tkinter.filedialog
# from namesConstant import zip_name
# from namesConstant import mcpack_name
import bgArrayLib.namesConstant
zipN = bgArrayLib.namesConstant.zip_name
mpN = bgArrayLib.namesConstant.mcpack_name
manifest = {
"format_version": 1,
"header": {
"name": "羽音缭绕-midiout_25.5--音创使用",
"description": "羽音缭绕-midiout_25.0--音创使用",
"uuid": "c1adbda4-3b3e-4e5b-a57e-cde8ac80ee19",
"version": [25, 5, 0],
},
"modules": [
{
"description": "羽音缭绕-midiout_25.0--音创使用",
"type": "resources",
"uuid": "c13455d5-b9f3-47f2-9706-c05ad86b3180 ",
"version": [25, 5, 0],
}
],
}
def resources_pathSetting(newPath: str = ""):
if not os.path.isfile("./bgArrayLib/resourcesPath.rpposi") and newPath == "":
return [False, 1] # 1:没有路径文件
elif newPath != "": # not os.path.isfile("./bgArrayLib/resourcesPath.rpposi") and
path = newPath
print(path)
with open("./bgArrayLib/resourcesPath.rpposi", "w") as w:
w.write(path)
if "mcpack(国际版推荐)格式_25.0" in os.listdir(
path
) and "zip格式_25.0" in os.listdir(path):
return [True, path, 1] # 1:都有
elif "mcpack(国际版推荐)格式_25.0" in os.listdir(
path
) and "zip格式_25.0" not in os.listdir(path):
return [True, path, 2] # 2:有pack
elif "mcpack(国际版推荐)格式_25.0" not in os.listdir(
path
) and "zip格式_25.0" in os.listdir(path):
return [True, path, 3] # 3:有zip
else:
return [False, 2] # 2:路径文件指示错误
if os.path.isfile("./bgArrayLib/resourcesPath.rpposi") and newPath == "":
with open("./bgArrayLib/resourcesPath.rpposi", "r") as f:
path = f.read()
if "mcpack(国际版推荐)格式_25.0" in os.listdir(
path
) and "zip格式_25.0" in os.listdir(path):
return [True, path, 1] # 1:都有
elif "mcpack(国际版推荐)格式_25.0" in os.listdir(
path
) and "zip格式_25.0" not in os.listdir(path):
return [True, path, 2] # 2:有pack
elif "mcpack(国际版推荐)格式_25.0" not in os.listdir(
path
) and "zip格式_25.0" in os.listdir(path):
return [True, path, 3] # 3:有zip
else:
return [False, 2] # 2:路径文件指示错误
raise
def choose_resources():
global zipN
global mpN
back_list = []
try:
with open(r"1.pkl", "rb") as rb:
instrument = list(pickle.load(rb))
print(instrument)
except FileNotFoundError:
with open(r"./nmcsup/1.pkl", "rb") as rb:
instrument = list(pickle.load(rb))
print(instrument)
path = resources_pathSetting()
if path.__len__() == 2:
return path
else:
dataT = path[2]
pathT = path[1]
if dataT == 1:
if instrument[1] is True: # 是否存在打击乐器
index = zipN.get(-1, "")
percussion_instrument = str(pathT) + "\\zip格式_25.0\\" + index
# print(percussion_instrument)
back_list.append(percussion_instrument)
for i in instrument[0]:
ins_p = str(pathT) + "\\zip格式_25.0\\" + str(zipN.get(i))
# print(ins_p)
back_list.append(ins_p)
print(back_list)
return back_list
elif dataT == 2:
if instrument[1] is True:
index = mpN.get(-1, "")
percussion_instrument = (
str(pathT) + "\\mcpack(国际版推荐)格式_25.0\\" + index
)
# print(percussion_instrument)
back_list.append(percussion_instrument)
for i in instrument[0]:
ins_p = str(pathT) + "\\mcpack(国际版推荐)格式_25.0\\" + str(mpN.get(i))
# print(ins_p)
back_list.append(ins_p)
print(back_list)
return back_list
elif dataT == 3:
if instrument[1] is True:
index = zipN.get(-1, "")
percussion_instrument = str(pathT) + "\\zip格式_25.0\\" + index
# print(percussion_instrument)
back_list.append(percussion_instrument)
for i in instrument[0]:
ins_p = str(pathT) + "\\zip格式_25.0\\" + str(zipN.get(i))
# print(ins_p)
back_list.append(ins_p)
print(back_list)
return back_list
raise
def scatteredPack(path):
pack_list = choose_resources()
print(pack_list)
print(path)
# os.close("L:/0WorldMusicCreater-MFMS new edition")
# shutil.copy("L:\\shenyu\\音源的资源包\\羽音缭绕-midiout_25.0\\mcpack(国际版推荐)格式_25.0\\0.Acoustic_Grand_Piano_大钢琴.mcpack",
# "L:/0WorldMusicCreater-MFMS new edition")
for i in pack_list:
shutil.copy(i, path)
if __name__ == "__main__":
# print(resources_pathSetting(r"L:\shenyu\音源的资源包\羽音缭绕-midiout_25.0"))
choose_resources()

View File

@@ -0,0 +1 @@
SEE: https://mingfengpigeon.mit-license.org/

View File

@@ -0,0 +1,5 @@
__all__ = ['Server', 'Plugin', 'build_header']
__version__ = '3.0.1'
__author__ = ['mingfengpigeon <mingfengpigeon@gmail.com>',"Eilles Wan <EillesWan@outlook.com>"]
from .server import Server, Plugin, build_header

View File

@@ -0,0 +1,142 @@
import asyncio
import copy
import json
import uuid
import websockets
class Server:
sent_commands = {}
subscribed_events = {}
_plugins = []
_connections = []
def __init__(self, server='0.0.0.0', port=8000, debug_mode=False):
self._server = server
self._port = port
self._debug_mode = debug_mode
def handler(self):
return copy.deepcopy(self._plugins)
def add_plugin(self, plugin):
if self._plugins:
for connection in self._connections:
plugin_ = plugin()
asyncio.create_task(plugin_.on_connect())
connection.append(plugin_)
self._plugins.append(plugin)
def remove_plugin(self, plugin):
if self._connections:
for connection in self._connections:
for plugin_ in connection.plugins:
if isinstance(plugin_, plugin):
plugin_.remove(plugin_)
break
self._plugins.remove(plugin)
async def run_forever(self):
self.running = True
async with websockets.serve(self._on_connect, self._server, self._port):
await asyncio.Future()
async def _on_connect(self, websocket, path):
plugins = []
self._connections.append({
"websocket": websocket,
"path": path,
"plugins": plugins,
})
for plugin in self._plugins:
plugins.append(plugin(websocket, path, self, self._debug_mode))
for plugin in plugins:
asyncio.create_task(plugin.on_connect())
while self.running:
try:
response = json.loads(await websocket.recv())
except (websockets.exceptions.ConnectionClosedOK, websockets.exceptions.ConnectionClosedError):
tasks = []
for plugin in plugins:
tasks.append(plugin.on_disconnect())
for task in tasks:
await task
break
else:
message_purpose = response['header']['messagePurpose']
if message_purpose == 'commandResponse':
request_id = response['header']['requestId']
if request_id in self.sent_commands:
asyncio.create_task(self.sent_commands[request_id](response))
del self.sent_commands[request_id]
else:
try:
event_name = response['header']['eventName']
asyncio.create_task(self.subscribed_events[event_name](response))
except KeyError:
print("ERROR EVENT NAME:\n{}".format(response))
async def disconnect(self, websocket: websockets.WebSocketServerProtocol):
self.running = False
await websocket.close_connection()
for number in range(len(self._connections) - 1):
connection = self._connections[number]
if connection['websocket'] == websocket:
del self._connections[number]
class Plugin:
def __init__(self, websocket, path, server, debug_mode=False):
self._websocket = websocket
self._path = path
self._server = server
self._debug_mode = debug_mode
async def on_connect(self):
pass
async def on_disconnect(self):
pass
async def on_receive(self, response):
pass
async def send_command(self, command, callback=None):
request = {
'body': {'commandLine': command},
'header': build_header('commandRequest')
}
if callback:
self._server.sent_commands[request['header']['requestId']] = callback
await self._websocket.send(json.dumps(request, **{'indent': 4} if self._debug_mode else {}))
async def subscribe(self, event_name, callback):
request = {
'body': {'eventName': event_name},
'header': build_header('subscribe')
}
self._server.subscribed_events[event_name] = callback
await self._websocket.send(json.dumps(request, **{'indent': 4} if self._debug_mode else {}))
async def unsubscribe(self, event_name):
request = {
'body': {'eventName': event_name},
'header': build_header('unsubscribe')
}
del self._server.subscribed_events[event_name]
await self._websocket.send(json.dumps(request, **{'indent': 4} if self._debug_mode else {}))
async def disconnect(self):
await self._server.disconnect(self._websocket)
def build_header(message_purpose, request_id=None):
if not request_id:
request_id = str(uuid.uuid4())
return {
'requestId': request_id,
'messagePurpose': message_purpose,
'version': '1',
'messageType': 'commandRequest',
}

View File

@@ -4,8 +4,8 @@
requires-python = ">= 3.8, < 4.0" requires-python = ">= 3.8, < 4.0"
dependencies = [ dependencies = [
"mido >= 1.3", "mido >= 1.3",
"tomli>=2.4.0; python_version < '3.11'", "tomli >= 2.4.0; python_version < '3.11'",
"tomli-w>=1.0.0", "tomli-w >= 1.0.0",
"xxhash >= 3", "xxhash >= 3",
] ]

View File

@@ -1,21 +0,0 @@
# 注意,这里是作者署名文件,文件格式开头为单子启
# 紧跟其后,不加空格留下常用名,常用名即常用网名
# 而在其后是各个语言下的名字。用 井字符 开头表示
# 注释,请注意,注释符号必须在一行之首否则无作用
# 每进行一次分段表示一个新的开发者,换行表示一个
# 新的语言。请全体开发者就此署名,谢谢!
启金羿
zh-CN 金羿
zh-TW 金羿
zh-ME 金羿羿喵
zh-HK 金 羿
en-GB Eilles
en-US EillesWan
启诸葛亮与八卦阵
zh-CN 诸葛亮与八卦阵
zh-TW 諸葛亮與八卦陣
zh-ME 诸葛八卦喵
zh-HK 諸葛亮與八卦陣
en-GB Bagua Array
en-US bgArray

View File

@@ -1,47 +0,0 @@
> 是谁把科技的领域布满政治的火药
>
> 是谁把纯净的蓝天染上暗淡的沉灰
>
> 中国人民无不热爱自己伟大的祖国
>
> 我们不会忘记屈辱历史留下的惨痛
>
> 我们希望世界和平
>
> 我们希望获得世界的尊重
>
> 愿世上再也没有战争
>
> 无论是热还是冷
>
> 无论是经济还是政治
>
> 让美妙的和平的优雅的音乐响彻世界
>
> ——金羿
> 2022 5 7
> Who has dropped political gunpowder into the technology
>
> Who has dyed clear blue sky into the dark grey
>
> All Chinese people love our great homeland
>
> We *WILL* remember the remain pain of the humiliating history
>
> We love the whole world but in peace
>
> We love everyone but under respect
>
> It is to be hoped that the war ends forever
>
> Whatever it is cold or hot
>
> Whatever it is economical or political
>
> Just let the wonderful music of peace surround the world
>
> ---- Eilles
> 7/5 2022

View File

@@ -11,4 +11,4 @@
不得用于商业用途 不得用于商业用途
若 音·创 库被用于商业用途,应当将其剔除 若 音·创 库被用于商业用途,应当将其剔除
版权所有 © 2025 诸葛亮与八卦阵 版权所有 © 2026 玉衡Alioth

18
test_read.py Normal file
View File

@@ -0,0 +1,18 @@
# 一个简单的项目实践测试
from pathlib import Path
from Musicreater import load_plugin_module, MusiCreater
from Musicreater.plugins import _global_plugin_registry
load_plugin_module("Musicreater.builtin_plugins.midi_read")
print("当前支持的导入格式:", _global_plugin_registry.supported_input_formats())
print("当前支持的导出格式:", _global_plugin_registry.supported_output_formats())
print(msct:=MusiCreater.import_music(Path("./resources/测试片段.mid")))
print(msct.music)
print(msct.midi_2_music_plugin.load(Path("./resources/测试片段.mid"), None))