mirror of
https://github.com/TriM-Organization/Musicreater.git
synced 2026-04-17 06:08:00 +00:00
完成 Midi 导入插件移植
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
存储 音·创 v3 的插件基类,提供抽象接口以供实际插件使用
|
||||
音·创 v3 的插件基类,提供抽象接口以供实际插件使用
|
||||
"""
|
||||
|
||||
"""
|
||||
@@ -42,6 +42,7 @@ from typing import (
|
||||
Iterator,
|
||||
Set,
|
||||
Type,
|
||||
Mapping,
|
||||
)
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
@@ -132,7 +133,7 @@ class PluginConfig(ABC):
|
||||
return {k: v for k, v in self.__dict__.items() if not k.startswith("_")}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> "PluginConfig":
|
||||
def from_dict(cls, data: Mapping[str, Any]) -> "PluginConfig":
|
||||
"""从字典创建配置实例
|
||||
|
||||
参数
|
||||
|
||||
32
Musicreater/_utils.py
Normal file
32
Musicreater/_utils.py
Normal file
@@ -0,0 +1,32 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
音·创 v3 的功能性内容合辑
|
||||
"""
|
||||
|
||||
"""
|
||||
版权所有 © 2026 金羿、玉衡Alioth
|
||||
Copyright © 2026 Eilles, YuhengAlioth
|
||||
|
||||
开源相关声明请见 仓库根目录下的 License.md
|
||||
Terms & Conditions: License.md in the root directory
|
||||
"""
|
||||
|
||||
# 睿乐组织 开发交流群 861684859
|
||||
# Email TriM-Organization@hotmail.com
|
||||
# 若需转载或借鉴 许可声明请查看仓库目录下的 License.md
|
||||
|
||||
|
||||
from copy import deepcopy, copy
|
||||
from typing import Any, Dict, Generator, List, Optional, Tuple, Union, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
def enumerated_stuffcopy_dictionary(
|
||||
enumeration_times: int = 17, staff: T = {}
|
||||
) -> Dict[int, T]:
|
||||
"""
|
||||
生成一个字典,其中键从0到enumeration_times-1,值是staff的拷贝
|
||||
"""
|
||||
# 这告诉我们,你不能忽略任何一个复制的序列,因为它真的,我哭死,折磨我一整天,全在这个bug上了
|
||||
# 上面的这指的是 copy.deepcopy —— 金羿 来自 20260210
|
||||
return {i: deepcopy(staff) for i in range(enumeration_times)}
|
||||
@@ -592,6 +592,8 @@ MM_DISLINK_PERCUSSION_INSTRUMENT_TABLE: Dict[int, str] = {
|
||||
|
||||
# NoteBlockStudio “NBS”音色对照表
|
||||
# https://github.com/OpenNBS/NoteBlockStudio/blob/main/scripts/midi_instruments/midi_instruments.gml
|
||||
# 此表来自于 Commit 1ab5357c197872495197f27ad8374d711b2a5195
|
||||
# 需要更新:https://github.com/OpenNBS/NoteBlockStudio/compare/main...development?diff=unified&w
|
||||
|
||||
MM_NBS_PITCHED_INSTRUMENT_TABLE: Dict[int, str] = {
|
||||
0: "note.harp",
|
||||
|
||||
69
Musicreater/builtin_plugins/midi_read/exceptions.py
Normal file
69
Musicreater/builtin_plugins/midi_read/exceptions.py
Normal file
@@ -0,0 +1,69 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
音·创 v3 内置的 Midi 读取插件用到的一些报错类型
|
||||
"""
|
||||
|
||||
"""
|
||||
版权所有 © 2026 金羿 & 玉衡Alioth
|
||||
Copyright © 2026 Eilles & YuhengAlioth
|
||||
|
||||
开源相关声明请见 仓库根目录下的 License.md
|
||||
Terms & Conditions: License.md in the root directory
|
||||
"""
|
||||
|
||||
# 睿乐组织 开发交流群 861684859
|
||||
# Email TriM-Organization@hotmail.com
|
||||
# 若需转载或借鉴 许可声明请查看仓库目录下的 License.md
|
||||
|
||||
|
||||
|
||||
from Musicreater.exceptions import MusicreaterOuterlyError
|
||||
|
||||
|
||||
class MidiFormatError(MusicreaterOuterlyError):
|
||||
"""音·创 的所有MIDI格式错误均继承于此"""
|
||||
|
||||
def __init__(self, *args):
|
||||
"""音·创 的所有MIDI格式错误均继承于此"""
|
||||
super().__init__("MIDI 格式错误 - ", *args)
|
||||
|
||||
|
||||
class NotDefineTempoError(MidiFormatError):
|
||||
"""没有Tempo设定导致时间无法计算的错误"""
|
||||
|
||||
def __init__(self, *args):
|
||||
"""没有Tempo设定导致时间无法计算的错误"""
|
||||
super().__init__("在曲目开始时没有声明 Tempo(未指定拍长):", *args)
|
||||
|
||||
|
||||
class ChannelOverFlowError(MidiFormatError):
|
||||
"""一个midi中含有过多的通道"""
|
||||
|
||||
def __init__(self, max_channel=16, *args):
|
||||
"""一个midi中含有过多的通道"""
|
||||
super().__init__("含有过多的通道(数量应≤{}):".format(max_channel), *args)
|
||||
|
||||
|
||||
class NotDefineProgramError(MidiFormatError):
|
||||
"""没有Program设定导致没有乐器可以选择的错误"""
|
||||
|
||||
def __init__(self, *args):
|
||||
"""没有Program设定导致没有乐器可以选择的错误"""
|
||||
super().__init__("未指定演奏乐器:", *args)
|
||||
|
||||
|
||||
class NoteOnOffMismatchError(MidiFormatError):
|
||||
"""音符开音和停止不匹配的错误"""
|
||||
|
||||
def __init__(self, *args):
|
||||
"""音符开音和停止不匹配的错误"""
|
||||
super().__init__("音符不匹配:", *args)
|
||||
|
||||
|
||||
class LyricMismatchError(MidiFormatError):
|
||||
"""歌词匹配解析错误"""
|
||||
|
||||
def __init__(self, *args):
|
||||
"""有可能产生了错误的歌词解析"""
|
||||
super().__init__("歌词解析错误:", *args)
|
||||
@@ -19,10 +19,11 @@ Terms & Conditions: License.md in the root directory
|
||||
import mido
|
||||
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import BinaryIO, Optional, Dict, List
|
||||
from typing import BinaryIO, Optional, Dict, List, Callable, Tuple, Mapping
|
||||
|
||||
from Musicreater import SingleMusic
|
||||
from Musicreater import SingleMusic, SingleTrack, SingleNote, SoundAtmos
|
||||
from Musicreater.plugins import (
|
||||
music_input_plugin,
|
||||
PluginConfig,
|
||||
@@ -30,10 +31,8 @@ from Musicreater.plugins import (
|
||||
PluginTypes,
|
||||
MusicInputPluginBase,
|
||||
)
|
||||
from Musicreater.types import (
|
||||
FittingFunctionType,
|
||||
)
|
||||
|
||||
from Musicreater.exceptions import ZeroSpeedError, IllegalMinimumVolumeError
|
||||
from Musicreater._utils import enumerated_stuffcopy_dictionary
|
||||
|
||||
from .constants import (
|
||||
MIDI_DEFAULT_PROGRAM_VALUE,
|
||||
@@ -41,26 +40,134 @@ from .constants import (
|
||||
MM_TOUCH_PERCUSSION_INSTRUMENT_TABLE,
|
||||
MM_TOUCH_PITCHED_INSTRUMENT_TABLE,
|
||||
)
|
||||
from .utils import velocity_2_distance_natural, panning_2_rotation_trigonometric
|
||||
from .exceptions import (
|
||||
MidiFormatError,
|
||||
NoteOnOffMismatchError,
|
||||
ChannelOverFlowError,
|
||||
LyricMismatchError,
|
||||
)
|
||||
from .utils import (
|
||||
volume_2_distance_natural,
|
||||
panning_2_rotation_trigonometric,
|
||||
midi_msgs_to_noteinfo,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class MidiImportConfig(PluginConfig):
|
||||
"""Midi 音乐数据导入插件配置"""
|
||||
|
||||
ignore_mismatch_error: bool = True
|
||||
speed: float = 1.0
|
||||
# 系统设置
|
||||
ignore_errors: bool = True
|
||||
|
||||
# 处理设置
|
||||
speed_multiplier: float = 1.0
|
||||
|
||||
# 兼容不良 Midi 所定义的默认值
|
||||
default_program_value: int = MIDI_DEFAULT_PROGRAM_VALUE
|
||||
default_volume_value: int = MIDI_DEFAULT_VOLUME_VALUE
|
||||
default_tempo_value: int = mido.midifiles.midifiles.DEFAULT_TEMPO
|
||||
pitched_note_rtable: Dict[int, str] = MM_TOUCH_PITCHED_INSTRUMENT_TABLE
|
||||
percussion_note_rtable: Dict[int, str] = MM_TOUCH_PERCUSSION_INSTRUMENT_TABLE
|
||||
vol_processing_function: FittingFunctionType = velocity_2_distance_natural
|
||||
pan_processing_function: FittingFunctionType = panning_2_rotation_trigonometric
|
||||
note_rtable_replacement: Dict[str, str] = {}
|
||||
|
||||
# 对照表
|
||||
pitched_note_reference_table: Mapping[int, str] = None # type: ignore
|
||||
percussion_note_reference_table: Mapping[int, str] = None # type: ignore
|
||||
note_replacement_table: Mapping[str, str] = None # type: ignore
|
||||
|
||||
# 参数转换函数
|
||||
volume_process_function: Callable[[float], float] = volume_2_distance_natural
|
||||
panning_processing_function: Callable[[float], float] = (
|
||||
panning_2_rotation_trigonometric
|
||||
)
|
||||
|
||||
# 分轨方式
|
||||
divide_tracks_by_miditrack: bool = True
|
||||
divide_tracks_by_midichannel: bool = False
|
||||
divide_tracks_by_soundname: bool = True
|
||||
divide_tracks_by_volume: bool = False
|
||||
divide_tracks_by_panning: bool = False
|
||||
|
||||
def __post_init__(self):
|
||||
self.pitched_note_reference_table = (
|
||||
self.pitched_note_reference_table
|
||||
if self.pitched_note_reference_table
|
||||
else MM_TOUCH_PITCHED_INSTRUMENT_TABLE
|
||||
)
|
||||
self.percussion_note_reference_table = (
|
||||
self.percussion_note_reference_table
|
||||
if self.percussion_note_reference_table
|
||||
else MM_TOUCH_PERCUSSION_INSTRUMENT_TABLE
|
||||
)
|
||||
self.note_replacement_table = (
|
||||
self.note_replacement_table if self.note_replacement_table else {}
|
||||
)
|
||||
|
||||
|
||||
@music_input_plugin("midi_2_music_by_tracks")
|
||||
class ControlerKeys(Enum):
|
||||
MIDI_PROGRAM = "midi_program"
|
||||
MIDI_VOLUME = "midi_volume"
|
||||
MIDI_PAN = "midi_pan"
|
||||
|
||||
|
||||
class TrackDivisionDict(
|
||||
Dict[
|
||||
Tuple[
|
||||
Optional[int],
|
||||
Optional[int],
|
||||
Optional[str],
|
||||
Optional[float],
|
||||
Optional[Tuple[float, float]],
|
||||
],
|
||||
SingleTrack,
|
||||
]
|
||||
):
|
||||
"""
|
||||
音轨分轨字典
|
||||
"""
|
||||
|
||||
division_by_miditrack: bool = True
|
||||
division_by_midichannel: bool = False
|
||||
division_by_soundname: bool = True
|
||||
division_by_volume: bool = False
|
||||
division_by_panning: bool = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*args,
|
||||
midi_import_config: MidiImportConfig = MidiImportConfig(),
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.division_by_miditrack = midi_import_config.divide_tracks_by_miditrack
|
||||
self.division_by_midichannel = midi_import_config.divide_tracks_by_midichannel
|
||||
self.division_by_soundname = midi_import_config.divide_tracks_by_soundname
|
||||
self.division_by_volume = midi_import_config.divide_tracks_by_volume
|
||||
self.division_by_panning = midi_import_config.divide_tracks_by_panning
|
||||
|
||||
def __getitem__(
|
||||
self,
|
||||
key: Tuple[
|
||||
Optional[int],
|
||||
Optional[int],
|
||||
Optional[str],
|
||||
Optional[float],
|
||||
Optional[Tuple[float, float]],
|
||||
],
|
||||
) -> SingleTrack:
|
||||
key = (
|
||||
key[0] if self.division_by_miditrack else None,
|
||||
key[1] if self.division_by_midichannel else None,
|
||||
key[2] if self.division_by_soundname else None,
|
||||
key[3] if self.division_by_volume else None,
|
||||
key[4] if self.division_by_panning else None,
|
||||
)
|
||||
try:
|
||||
return super().__getitem__(key)
|
||||
except KeyError:
|
||||
self[key] = SingleTrack()
|
||||
return self[key]
|
||||
|
||||
|
||||
@music_input_plugin("midi_2_music_plugin")
|
||||
class MidiImport2MusicPlugin(MusicInputPluginBase):
|
||||
"""Midi 音乐数据导入插件"""
|
||||
|
||||
@@ -76,14 +183,305 @@ class MidiImport2MusicPlugin(MusicInputPluginBase):
|
||||
supported_formats = ("MID", "MIDI")
|
||||
|
||||
def loadbytes(
|
||||
self, bytes_buffer_in: BinaryIO, config: MidiImportConfig = MidiImportConfig()
|
||||
self,
|
||||
bytes_buffer_in: BinaryIO,
|
||||
config: Optional[MidiImportConfig] = MidiImportConfig(),
|
||||
) -> SingleMusic:
|
||||
midi_file = mido.MidiFile(file=bytes_buffer_in)
|
||||
return SingleMusic() # =========================== TODO: 等待制作
|
||||
return self.midifile_2_singlemusic(
|
||||
mido.MidiFile(file=bytes_buffer_in),
|
||||
config if config else MidiImportConfig(),
|
||||
)
|
||||
|
||||
def load(
|
||||
self, file_path: Path, config: MidiImportConfig = MidiImportConfig()
|
||||
) -> "SingleMusic":
|
||||
self, file_path: Path, config: Optional[MidiImportConfig] = MidiImportConfig()
|
||||
) -> SingleMusic:
|
||||
"""从 Midi 文件导入音乐数据"""
|
||||
midi_file = mido.MidiFile(filename=file_path)
|
||||
return SingleMusic() # =========================== TODO: 等待制作
|
||||
return self.midifile_2_singlemusic(
|
||||
mido.MidiFile(filename=file_path), config if config else MidiImportConfig()
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def midifile_2_singlemusic(
|
||||
midi: mido.MidiFile,
|
||||
config: MidiImportConfig = MidiImportConfig(),
|
||||
) -> SingleMusic:
|
||||
"""
|
||||
将midi解析并转换为频道音符字典
|
||||
|
||||
Parameters
|
||||
----------
|
||||
midi: mido.MidiFile 对象
|
||||
需要处理的midi对象
|
||||
speed: float
|
||||
音乐播放速度倍数
|
||||
default_program_value: int
|
||||
默认的 MIDI 乐器值
|
||||
default_volume_value: int
|
||||
默认的通道音量值
|
||||
default_tempo_value: int
|
||||
默认的 MIDI TEMPO 值
|
||||
pitched_note_rtable: Dict[int, Tuple[str, int]]
|
||||
乐音乐器Midi-MC对照表
|
||||
percussion_note_rtable: Dict[int, Tuple[str, int]]
|
||||
打击乐器Midi-MC对照表
|
||||
vol_processing_function: Callable[[float], float]
|
||||
音量对播放距离的拟合函数
|
||||
pan_processing_function: Callable[[float], float]
|
||||
声像偏移对播放旋转角度的拟合函数
|
||||
note_rtable_replacement: Dict[str, str]
|
||||
音符名称替换表,此表用于对 Minecraft 乐器名称进行替换,而非 Midi Program 的替换
|
||||
|
||||
Returns
|
||||
-------
|
||||
Tuple[SingleMusic, int, Dict[str, int]]
|
||||
以通道作为分割的Midi音符列表字典, 音符总数, 乐器使用统计
|
||||
"""
|
||||
|
||||
if config.speed_multiplier == 0:
|
||||
raise ZeroSpeedError("播放速度不得为零,应为 (0,1] 范围内的实数。")
|
||||
|
||||
# 一个midi中仅有16个通道 我们通过通道来识别而不是音轨
|
||||
divided_tracks: TrackDivisionDict = TrackDivisionDict(midi_import_config=config)
|
||||
|
||||
value_controler_per_channel: Dict[int, Dict[ControlerKeys, int]] = (
|
||||
enumerated_stuffcopy_dictionary(
|
||||
staff={
|
||||
ControlerKeys.MIDI_PROGRAM: config.default_program_value,
|
||||
ControlerKeys.MIDI_VOLUME: config.default_volume_value,
|
||||
ControlerKeys.MIDI_PAN: 64,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
midi_tempo = config.default_tempo_value
|
||||
"""微秒每拍"""
|
||||
note_count = 0
|
||||
"""音符计数"""
|
||||
note_count_per_instrument: Dict[str, int] = {}
|
||||
"""乐器使用统计"""
|
||||
microseconds = 0
|
||||
"""当前的微妙时间"""
|
||||
|
||||
note_queue_A: Dict[int, List[Tuple[int, int]]] = (
|
||||
enumerated_stuffcopy_dictionary(staff=[])
|
||||
)
|
||||
"""音符队列甲 Dict[通道, List[Tuple[int音高, int乐器, int轨道]]]"""
|
||||
note_queue_B: Dict[int, List[Tuple[int, int, int]]] = (
|
||||
enumerated_stuffcopy_dictionary(staff=[])
|
||||
)
|
||||
"""音符队列乙 Dict[通道, List[Tuple[int音高, int微秒时间]]]"""
|
||||
|
||||
midi_lyric_cache: List[Tuple[int, str]] = []
|
||||
"""歌词缓存 List[Tuple[int微秒时间, str歌词内容]]"""
|
||||
|
||||
midi_text_list: List[str] = []
|
||||
"""Midi 附加文本列表"""
|
||||
midi_copyright_list: List[str] = []
|
||||
"""Midi 版权列表"""
|
||||
midi_track_name_dict: Dict[int, str] = {}
|
||||
"""轨道名称字典 Dict[int轨道编号, str轨道名称]"""
|
||||
|
||||
for track_no, message_track in enumerate(midi.tracks):
|
||||
for msg in message_track:
|
||||
if msg.type == "set_tempo":
|
||||
midi_tempo = msg.tempo
|
||||
|
||||
if msg.time != 0:
|
||||
# 微秒
|
||||
# 通常情况下,tempo 是 500000,tpb 在
|
||||
microseconds += msg.time * midi_tempo / midi.ticks_per_beat
|
||||
|
||||
if msg.type == "program_change":
|
||||
# 检测 乐器变化 之 midi 事件
|
||||
value_controler_per_channel[msg.channel][
|
||||
ControlerKeys.MIDI_PROGRAM
|
||||
] = msg.program
|
||||
|
||||
elif msg.is_cc(7):
|
||||
# Control Change 更改当前通道的 音量 的事件(大幅度,最高有效位)
|
||||
value_controler_per_channel[msg.channel][
|
||||
ControlerKeys.MIDI_VOLUME
|
||||
] = msg.value
|
||||
elif msg.is_cc(10):
|
||||
# Control Change 更改当前通道的 音调偏移 的事件(大幅度,最高有效位)
|
||||
value_controler_per_channel[msg.channel][
|
||||
ControlerKeys.MIDI_PAN
|
||||
] = msg.value
|
||||
|
||||
elif msg.type == "lyrics":
|
||||
# 歌词事件
|
||||
midi_lyric_cache.append((microseconds, msg.text))
|
||||
# print(lyric_cache, flush=True)
|
||||
elif msg.type == "text":
|
||||
# 检测文本事件
|
||||
midi_text_list.append(msg.text)
|
||||
elif msg.type == "copyright":
|
||||
# 检测版权事件
|
||||
midi_copyright_list.append(msg.text)
|
||||
elif msg.type == "track_name":
|
||||
# 检测轨道名称事件
|
||||
midi_track_name_dict[track_no] = msg.name
|
||||
elif msg.type == "note_on" and msg.velocity != 0:
|
||||
# 一个音符开始弹奏
|
||||
|
||||
# 加入音符队列甲(按通道分隔)
|
||||
# (音高, 轨道)
|
||||
note_queue_A[msg.channel].append((msg.note, track_no))
|
||||
# 音符队列乙(按通道分隔)
|
||||
# (乐器, 力度, 微秒)
|
||||
note_queue_B[msg.channel].append(
|
||||
(
|
||||
value_controler_per_channel[msg.channel][
|
||||
ControlerKeys.MIDI_PROGRAM
|
||||
],
|
||||
msg.velocity,
|
||||
microseconds,
|
||||
)
|
||||
)
|
||||
|
||||
elif (msg.type == "note_off") or (
|
||||
msg.type == "note_on" and msg.velocity == 0
|
||||
):
|
||||
# 一个音符结束弹奏
|
||||
|
||||
if (
|
||||
msg.note,
|
||||
value_controler_per_channel[msg.channel][
|
||||
ControlerKeys.MIDI_PROGRAM
|
||||
],
|
||||
) in note_queue_A[msg.channel]:
|
||||
# 在甲队列中发现了同一个 音高和乐器且在同轨道 的音符
|
||||
|
||||
# 获取其音符力度和微秒数
|
||||
_velocity, _program, _ms = note_queue_B[msg.channel][
|
||||
note_queue_A[msg.channel].index((msg.note, track_no))
|
||||
]
|
||||
|
||||
# 在队列中删除此音符
|
||||
note_queue_A[msg.channel].remove((msg.note, track_no))
|
||||
note_queue_B[msg.channel].remove((_velocity, _program, _ms))
|
||||
|
||||
_lyric = ""
|
||||
# 找一找歌词吧
|
||||
if midi_lyric_cache:
|
||||
for i in range(len(midi_lyric_cache)):
|
||||
if midi_lyric_cache[i][0] >= _ms:
|
||||
_lyric = midi_lyric_cache.pop(i)[1]
|
||||
break
|
||||
|
||||
# 更新结果信息
|
||||
|
||||
that_note, sound_name, orign_distance, sound_rotation = (
|
||||
midi_msgs_to_noteinfo(
|
||||
inst=(
|
||||
msg.note
|
||||
if (_is_percussion := (msg.channel == 9))
|
||||
else _program
|
||||
),
|
||||
note=(_program if _is_percussion else msg.note),
|
||||
percussive=_is_percussion,
|
||||
volume=value_controler_per_channel[msg.channel][
|
||||
ControlerKeys.MIDI_VOLUME
|
||||
],
|
||||
velocity=_velocity,
|
||||
panning=value_controler_per_channel[msg.channel][
|
||||
ControlerKeys.MIDI_PAN
|
||||
],
|
||||
start_time=_ms, # 微秒
|
||||
duration=microseconds - _ms, # 微秒
|
||||
play_speed=config.speed_multiplier,
|
||||
midi_reference_table=(
|
||||
config.percussion_note_reference_table
|
||||
if _is_percussion
|
||||
else config.pitched_note_reference_table
|
||||
),
|
||||
volume_processing_method=config.volume_process_function,
|
||||
panning_processing_method=config.panning_processing_function,
|
||||
note_table_replacement=config.note_replacement_table,
|
||||
lyric_line=_lyric,
|
||||
)
|
||||
)
|
||||
|
||||
divided_tracks[
|
||||
(
|
||||
track_no,
|
||||
msg.channel,
|
||||
sound_name,
|
||||
orign_distance,
|
||||
sound_rotation,
|
||||
)
|
||||
].add(that_note)
|
||||
|
||||
# 更新统计信息
|
||||
note_count += 1
|
||||
if sound_name in note_count_per_instrument.keys():
|
||||
note_count_per_instrument[sound_name] += 1
|
||||
else:
|
||||
note_count_per_instrument[sound_name] = 1
|
||||
|
||||
else:
|
||||
# 什么?找不到 note on 消息??
|
||||
if config.ignore_errors:
|
||||
print(
|
||||
"[WARRING] MIDI格式错误 音符不匹配`{}`无法在上文`{}`中找到与之匹配的音符开音消息".format(
|
||||
msg, note_queue_A[msg.channel]
|
||||
)
|
||||
)
|
||||
else:
|
||||
raise NoteOnOffMismatchError(
|
||||
"当前的MIDI很可能有损坏之嫌……",
|
||||
msg,
|
||||
"无法在上文中找到与之匹配的音符开音消息。",
|
||||
)
|
||||
|
||||
"""整合后的音乐通道格式
|
||||
每个通道包括若干消息元素其中逃不过这三种:
|
||||
|
||||
1 切换乐器消息
|
||||
("PgmC", 切换后的乐器ID: int, 距离演奏开始的毫秒)
|
||||
|
||||
2 音符开始消息
|
||||
("NoteS", 开始的音符ID, 力度(响度), 距离演奏开始的毫秒)
|
||||
|
||||
3 音符结束消息
|
||||
("NoteE", 结束的音符ID, 距离演奏开始的毫秒)"""
|
||||
|
||||
del midi_tempo
|
||||
|
||||
if midi_lyric_cache:
|
||||
# 怎么有歌词多啊
|
||||
if config.ignore_errors:
|
||||
print(
|
||||
"[WARRING] MIDI 解析错误 歌词对应错误,以下歌词未能填入音符之中,已经填入的仍可能有误 {}".format(
|
||||
midi_lyric_cache
|
||||
)
|
||||
)
|
||||
else:
|
||||
raise LyricMismatchError(
|
||||
"MIDI 解析产生错误",
|
||||
"歌词解析过程中无法对应音符,已填入的音符仍可能有误",
|
||||
midi_lyric_cache,
|
||||
)
|
||||
|
||||
final_music = SingleMusic(
|
||||
credits="; ".join(midi_copyright_list),
|
||||
extra_information={
|
||||
"MIDI_TEXT_LIST": midi_text_list,
|
||||
"NOTE_COUNT": note_count,
|
||||
"NOTE_COUNT_PER_INSTRUMENT": note_count_per_instrument,
|
||||
},
|
||||
)
|
||||
for track_properties, every_single_track in divided_tracks.items():
|
||||
if track_properties[0] and (
|
||||
track_name := midi_track_name_dict.get(track_properties[0])
|
||||
):
|
||||
every_single_track.track_name = track_name
|
||||
if track_properties[2]:
|
||||
every_single_track.track_instrument = track_properties[2]
|
||||
if track_properties[3]:
|
||||
every_single_track.sound_position.sound_distance = track_properties[3]
|
||||
if track_properties[4]:
|
||||
every_single_track.sound_position.sound_azimuth = track_properties[4]
|
||||
final_music.append(every_single_track)
|
||||
|
||||
return final_music
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
@@ -20,18 +19,21 @@ Terms & Conditions: License.md in the root directory
|
||||
|
||||
import math
|
||||
|
||||
from typing import Callable, Dict, List, Optional, Sequence, Tuple, Mapping
|
||||
|
||||
from Musicreater import SingleNote, SoundAtmos
|
||||
|
||||
|
||||
def velocity_2_distance_natural(
|
||||
def volume_2_distance_natural(
|
||||
vol: float,
|
||||
) -> float:
|
||||
"""
|
||||
midi力度值拟合成的距离函数
|
||||
Midi 力度值/音量值拟合成的距离函数,一种更加自然的听感?
|
||||
|
||||
Parameters
|
||||
----------
|
||||
vol: int
|
||||
midi 音符力度值
|
||||
Midi 音符力度值(0~127)
|
||||
|
||||
Returns
|
||||
-------
|
||||
@@ -50,20 +52,20 @@ def velocity_2_distance_natural(
|
||||
)
|
||||
|
||||
|
||||
def velocity_2_distance_straight(vol: float) -> float:
|
||||
def volume_2_distance_straight(vol: float) -> float:
|
||||
"""
|
||||
midi力度值拟合成的距离函数
|
||||
Midi 力度值/音量值拟合成的距离函数,线性转换
|
||||
|
||||
Parameters
|
||||
----------
|
||||
vol: int
|
||||
midi 音符力度值
|
||||
Midi 音符力度值(0~127)
|
||||
|
||||
Returns
|
||||
-------
|
||||
float播放中心到玩家的距离
|
||||
"""
|
||||
return vol / -8 + 16
|
||||
return (vol + 1) / -8 + 16
|
||||
|
||||
|
||||
def panning_2_rotation_linear(pan_: float) -> float:
|
||||
@@ -106,3 +108,115 @@ def panning_2_rotation_trigonometric(pan_: float) -> float:
|
||||
else:
|
||||
return math.degrees(math.acos((64 - pan_) / 63)) - 90
|
||||
|
||||
|
||||
def midi_inst_to_mc_sound(
|
||||
instrumentID: int,
|
||||
reference_table: Mapping[int, str],
|
||||
default_instrument: str = "note.flute",
|
||||
) -> str:
|
||||
"""
|
||||
返回midi的乐器ID对应的我的世界乐器名
|
||||
|
||||
Parameters
|
||||
----------
|
||||
instrumentID: int
|
||||
midi的乐器ID
|
||||
reference_table: Dict[int, Tuple[str, int]]
|
||||
转换乐器参照表
|
||||
default_instrument: str
|
||||
查无此乐器时的替换乐器
|
||||
|
||||
Returns
|
||||
-------
|
||||
str我的世界乐器名
|
||||
"""
|
||||
return reference_table.get(
|
||||
instrumentID,
|
||||
default_instrument,
|
||||
)
|
||||
|
||||
|
||||
def midi_msgs_to_noteinfo(
|
||||
inst: int, # 乐器编号
|
||||
note: int,
|
||||
percussive: bool, # 是否作为打击乐器启用
|
||||
volume: int,
|
||||
velocity: int,
|
||||
panning: int,
|
||||
start_time: int,
|
||||
duration: int,
|
||||
play_speed: float,
|
||||
midi_reference_table: Mapping[int, str],
|
||||
volume_processing_method: Callable[[float], float],
|
||||
panning_processing_method: Callable[[float], float],
|
||||
note_table_replacement: Mapping[str, str] = {},
|
||||
lyric_line: str = "",
|
||||
) -> Tuple[SingleNote, str, float, Tuple[float, float]]:
|
||||
"""
|
||||
将 Midi信息转为音符对象
|
||||
|
||||
Parameters
|
||||
------------
|
||||
inst: int
|
||||
乐器编号
|
||||
note: int
|
||||
音高编号(音符编号)
|
||||
percussive: bool
|
||||
是否作为打击乐器启用
|
||||
volume: int
|
||||
音量
|
||||
velocity: int
|
||||
力度
|
||||
panning: int
|
||||
声相偏移
|
||||
start_time: int
|
||||
音符起始时间(微秒)
|
||||
duration: int
|
||||
音符持续时间(微秒)
|
||||
play_speed: float
|
||||
曲目播放速度
|
||||
midi_reference_table: Dict[int, str]
|
||||
转换对照表
|
||||
volume_processing_method: Callable[[float], float]
|
||||
音量处理函数
|
||||
panning_processing_method: Callable[[float], float]
|
||||
立体声相偏移处理函数
|
||||
note_table_replacement: Dict[str, str]
|
||||
音符替换表,定义 Minecraft 音符字串的替换
|
||||
lyric_line: str
|
||||
该音符的歌词
|
||||
|
||||
Returns
|
||||
---------
|
||||
Tuple[
|
||||
MineNote我的世界音符对象,
|
||||
str我的世界声音名,
|
||||
float播放中心到玩家的距离,
|
||||
Tuple[float, float]声源旋转角度
|
||||
]
|
||||
"""
|
||||
mc_sound_ID = midi_inst_to_mc_sound(
|
||||
inst,
|
||||
midi_reference_table,
|
||||
"note.bd" if percussive else "note.flute",
|
||||
)
|
||||
|
||||
return (
|
||||
SingleNote(
|
||||
midi_pitch=note,
|
||||
note_volume=int((velocity / 127) + 0.5),
|
||||
start_tick=(tk := int(start_time / float(play_speed) / 50000)),
|
||||
keep_tick=round(duration / float(play_speed) / 50000),
|
||||
mass_precision_time=round(
|
||||
(start_time / float(play_speed) - tk * 50000) / 800
|
||||
),
|
||||
extra_information={
|
||||
"LYRIC_TEXT": lyric_line,
|
||||
"VOLUME_VALUE": volume,
|
||||
"PIN_VALUE": panning,
|
||||
},
|
||||
),
|
||||
note_table_replacement.get(mc_sound_ID, mc_sound_ID),
|
||||
volume_processing_method(volume),
|
||||
(panning_processing_method(panning), 0),
|
||||
)
|
||||
|
||||
@@ -34,15 +34,6 @@ z = "z"
|
||||
z
|
||||
"""
|
||||
|
||||
MIDI_PROGRAM = "program"
|
||||
"""Midi乐器编号"""
|
||||
|
||||
MIDI_VOLUME = "volume"
|
||||
"""Midi通道音量"""
|
||||
|
||||
MIDI_PAN = "pan"
|
||||
"""Midi通道立体声场偏移"""
|
||||
|
||||
|
||||
# Midi用对照表
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
存储 音·创 v3 的内部数据类
|
||||
音·创 v3 的内部数据类
|
||||
"""
|
||||
|
||||
"""
|
||||
@@ -41,6 +41,7 @@ from typing import (
|
||||
Literal,
|
||||
Hashable,
|
||||
TypeVar,
|
||||
Mapping,
|
||||
)
|
||||
from enum import Enum
|
||||
|
||||
@@ -71,12 +72,14 @@ class SoundAtmos:
|
||||
------------
|
||||
distance: float
|
||||
发声源距离玩家的距离(半径 `r`)
|
||||
注:距离越近,音量越高,默认为 0。此参数可以与音量成某种函数关系。
|
||||
注:距离越近,音量越高,默认为 0。此参数可以作为音轨的音量使用。
|
||||
音量若默认为 +0,则此值当为 8;此值最小为 0.01,最大为 16。
|
||||
azimuth: tuple[float, float]
|
||||
声源方位
|
||||
注:此参数为tuple,包含两个元素,分别表示:
|
||||
`rV` 发声源在竖直(上下)轴上,从玩家视角正前方开始,向顺时针旋转的角度
|
||||
`rH` 发声源在水平(左右)轴上,从玩家视角正前方开始,向上(到达玩家正上方顶点后变为向下,以此类推的旋转)旋转的角度
|
||||
`rH` 发声源在水平(左右)轴上,从玩家视角正前方开始,向上
|
||||
(到达玩家正上方顶点后变为向下,以此类推的旋转)旋转的角度
|
||||
"""
|
||||
|
||||
self.sound_azimuth = (azimuth[0] % 360, azimuth[1] % 360) if azimuth else (0, 0)
|
||||
@@ -131,8 +134,8 @@ class SingleNote:
|
||||
note_pitch: int
|
||||
"""midi音高"""
|
||||
|
||||
velocity: int
|
||||
"""力度"""
|
||||
volume: int
|
||||
"""力度/播放响度 0~100 百分比"""
|
||||
|
||||
start_time: int
|
||||
"""开始之时 命令刻"""
|
||||
@@ -149,9 +152,9 @@ class SingleNote:
|
||||
def __init__(
|
||||
self,
|
||||
midi_pitch: Optional[int],
|
||||
midi_velocity: int,
|
||||
start_time: int,
|
||||
last_time: int,
|
||||
note_volume: int,
|
||||
start_tick: int,
|
||||
keep_tick: int,
|
||||
mass_precision_time: int = 0,
|
||||
extra_information: Dict[str, Any] = {},
|
||||
):
|
||||
@@ -162,8 +165,8 @@ class SingleNote:
|
||||
------------
|
||||
midi_pitch: int
|
||||
midi音高
|
||||
midi_velocity: int
|
||||
midi响度(力度)
|
||||
note_volume: int
|
||||
响度/力度(百分比, 0~100)
|
||||
start_time: int
|
||||
开始之时(命令刻)
|
||||
注:此处的时间是用从乐曲开始到当前的刻数
|
||||
@@ -183,11 +186,11 @@ class SingleNote:
|
||||
|
||||
self.note_pitch: int = 66 if midi_pitch is None else midi_pitch
|
||||
"""midi音高"""
|
||||
self.velocity: int = midi_velocity
|
||||
self.volume: int = note_volume
|
||||
"""响度(力度)"""
|
||||
self.start_time: int = start_time
|
||||
self.start_time: int = start_tick
|
||||
"""开始之时 命令刻"""
|
||||
self.duration: int = last_time
|
||||
self.duration: int = keep_tick
|
||||
"""音符持续时间 命令刻"""
|
||||
self.high_precision_start_time: int = mass_precision_time
|
||||
"""高精度开始时间偏量 0.4 毫秒"""
|
||||
@@ -201,15 +204,15 @@ class SingleNote:
|
||||
group_1 := int.from_bytes(code_buffer[:6], "big")
|
||||
) & 0b11111111111111111
|
||||
start_tick_ = (group_1 := group_1 >> 17) & 0b11111111111111111
|
||||
note_velocity_ = (group_1 := group_1 >> 17) & 0b1111111
|
||||
note_volume_ = (group_1 := group_1 >> 17) & 0b1111111
|
||||
note_pitch_ = (group_1 := group_1 >> 7) & 0b1111111
|
||||
|
||||
try:
|
||||
return cls(
|
||||
midi_pitch=note_pitch_,
|
||||
midi_velocity=note_velocity_,
|
||||
start_time=start_tick_,
|
||||
last_time=duration_,
|
||||
note_volume=note_volume_,
|
||||
start_tick=start_tick_,
|
||||
keep_tick=duration_,
|
||||
mass_precision_time=code_buffer[6] if is_high_time_precision else 0,
|
||||
)
|
||||
except Exception as e:
|
||||
@@ -245,7 +248,7 @@ class SingleNote:
|
||||
# SingleNote 的字节码
|
||||
|
||||
# note_pitch 7 位 支持到 127
|
||||
# velocity 长度 7 位 支持到 127
|
||||
# volume 长度 7 位 支持到 127
|
||||
# start_tick 17 位 支持到 131071 即 109.22583 分钟 合 1.8204305 小时
|
||||
# duration 17 位 支持到 131071 即 109.22583 分钟 合 1.8204305 小时
|
||||
# 共 48 位 合 6 字节
|
||||
@@ -255,7 +258,7 @@ class SingleNote:
|
||||
return (
|
||||
(
|
||||
(
|
||||
((((self.note_pitch << 7) + self.velocity) << 17) + self.start_time)
|
||||
((((self.note_pitch << 7) + self.volume) << 17) + self.start_time)
|
||||
<< 17
|
||||
)
|
||||
+ self.duration
|
||||
@@ -290,9 +293,9 @@ class SingleNote:
|
||||
return self.extra_info.get(key, default)
|
||||
|
||||
def stringize(self, include_extra_data: bool = False) -> str:
|
||||
return "TrackedNote(Pitch = {}, Velocity = {}, StartTick = {}, Duration = {}, TimeOffset = {}".format(
|
||||
return "TrackedNote(Pitch = {}, Volume = {}, StartTick = {}, Duration = {}, TimeOffset = {}".format(
|
||||
self.note_pitch,
|
||||
self.velocity,
|
||||
self.volume,
|
||||
self.start_time,
|
||||
self.duration,
|
||||
self.high_precision_start_time,
|
||||
@@ -308,7 +311,7 @@ class SingleNote:
|
||||
) -> Tuple[int, int, int, int, int]:
|
||||
return (
|
||||
self.note_pitch,
|
||||
self.velocity,
|
||||
self.volume,
|
||||
self.start_time,
|
||||
self.duration,
|
||||
self.high_precision_start_time,
|
||||
@@ -317,39 +320,38 @@ class SingleNote:
|
||||
def __dict__(self):
|
||||
return {
|
||||
"Pitch": self.note_pitch,
|
||||
"Velocity": self.velocity,
|
||||
"Volume": self.volume,
|
||||
"StartTick": self.start_time,
|
||||
"Duration": self.duration,
|
||||
"TimeOffset": self.high_precision_start_time,
|
||||
"ExtraData": self.extra_info,
|
||||
}
|
||||
|
||||
def __eq__(self, other) -> bool:
|
||||
def __eq__(self, other: "SingleNote") -> bool:
|
||||
"""比较两个音符是否具有相同的属性,不计附加信息"""
|
||||
if not isinstance(other, self.__class__):
|
||||
return False
|
||||
return self.__tuple__() == other.__tuple__()
|
||||
|
||||
def __lt__(self, other) -> bool:
|
||||
def __lt__(self, other: "SingleNote") -> bool:
|
||||
"""比较自己是否在开始时间上早于另一个音符"""
|
||||
if self.start_time == other.start_tick:
|
||||
return self.high_precision_start_time < other.high_precision_time
|
||||
if self.start_time == other.start_time:
|
||||
return self.high_precision_start_time < other.high_precision_start_time
|
||||
else:
|
||||
return self.start_time < other.start_tick
|
||||
return self.start_time < other.start_time
|
||||
|
||||
def __gt__(self, other) -> bool:
|
||||
def __gt__(self, other: "SingleNote") -> bool:
|
||||
"""比较自己是否在开始时间上晚于另一个音符"""
|
||||
if self.start_time == other.start_tick:
|
||||
return self.high_precision_start_time > other.high_precision_time
|
||||
if self.start_time == other.start_time:
|
||||
return self.high_precision_start_time > other.high_precision_start_time
|
||||
else:
|
||||
return self.start_time > other.start_tick
|
||||
return self.start_time > other.start_time
|
||||
|
||||
|
||||
class CurvableParam(str, Enum):
|
||||
"""可曲线化的参数枚举类"""
|
||||
|
||||
PITCH = "adjust_note_pitch"
|
||||
VELOCITY = "adjust_note_velocity"
|
||||
VOLUME = "adjust_note_volume"
|
||||
DISTANCE = "adjust_note_sound_distance"
|
||||
LR_PANNING = "adjust_note_leftright_panning_degree"
|
||||
@@ -361,13 +363,11 @@ class MineNote:
|
||||
"""我的世界音符对象(仅提供我的世界相关接口)"""
|
||||
|
||||
pitch: float
|
||||
"""midi音高"""
|
||||
"""Midi 音高"""
|
||||
instrument: str
|
||||
"""乐器ID"""
|
||||
velocity: float
|
||||
"""力度"""
|
||||
"""乐器 ID"""
|
||||
volume: float
|
||||
"""音量"""
|
||||
"""力度/播放音量 0~100 百分比"""
|
||||
start_tick: int
|
||||
"""开始之时 命令刻"""
|
||||
duration_tick: int
|
||||
@@ -384,12 +384,10 @@ class MineNote:
|
||||
cls,
|
||||
note: SingleNote,
|
||||
note_instrument: str,
|
||||
sound_volume: float,
|
||||
is_persiced_time: bool,
|
||||
is_percussive_note: bool,
|
||||
sound_position: SoundAtmos,
|
||||
adjust_note_pitch: float = 0.0,
|
||||
adjust_note_velocity: float = 0.0,
|
||||
adjust_note_volume: float = 0.0,
|
||||
adjust_note_sound_distance: float = 0.0,
|
||||
adjust_note_leftright_panning_degree: float = 0.0,
|
||||
@@ -404,8 +402,7 @@ class MineNote:
|
||||
return cls(
|
||||
pitch=note.note_pitch + adjust_note_pitch,
|
||||
instrument=note_instrument,
|
||||
velocity=note.velocity + adjust_note_velocity,
|
||||
volume=sound_volume + adjust_note_volume,
|
||||
volume=note.volume + adjust_note_volume,
|
||||
start_tick=note.start_time,
|
||||
duration_tick=note.duration,
|
||||
persiced_time=note.high_precision_start_time if is_persiced_time else 0,
|
||||
@@ -426,9 +423,6 @@ class SingleTrack(List[SingleNote]):
|
||||
track_instrument: str
|
||||
"""乐器ID"""
|
||||
|
||||
track_volume: float
|
||||
"""该音轨的音量"""
|
||||
|
||||
is_high_time_precision: bool
|
||||
"""该音轨是否使用高精度时间"""
|
||||
|
||||
@@ -446,14 +440,13 @@ class SingleTrack(List[SingleNote]):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*args: SingleNote,
|
||||
name: str = "未命名音轨",
|
||||
instrument: str = "",
|
||||
volume: float = 0,
|
||||
precise_time: bool = True,
|
||||
percussion: bool = False,
|
||||
sound_direction: SoundAtmos = SoundAtmos(),
|
||||
extra_information: Dict[str, Any] = {},
|
||||
*args: SingleNote,
|
||||
):
|
||||
self.track_name = name
|
||||
"""音轨名称"""
|
||||
@@ -461,9 +454,6 @@ class SingleTrack(List[SingleNote]):
|
||||
self.track_instrument = instrument
|
||||
"""乐器ID"""
|
||||
|
||||
self.track_volume = volume
|
||||
"""音量"""
|
||||
|
||||
self.is_high_time_precision = precise_time
|
||||
"""是否使用高精度时间"""
|
||||
|
||||
@@ -559,7 +549,6 @@ class SingleTrack(List[SingleNote]):
|
||||
yield MineNote.from_single_note(
|
||||
note=_note,
|
||||
note_instrument=self.track_instrument,
|
||||
sound_volume=self.track_volume,
|
||||
is_persiced_time=self.is_high_time_precision,
|
||||
is_percussive_note=self.is_percussive,
|
||||
sound_position=self.sound_position,
|
||||
@@ -626,12 +615,12 @@ class SingleMusic(List[SingleTrack]):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*args: SingleTrack,
|
||||
name: str = "未命名乐曲",
|
||||
creator: str = "未命名制作者",
|
||||
original_author: str = "未命名原作者",
|
||||
description: str = "未命名简介",
|
||||
credits: str = "未命名版权信息",
|
||||
*args: SingleTrack,
|
||||
extra_information: Dict[str, Any] = {},
|
||||
):
|
||||
self.music_name = name
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
存储 音·创 v3 用到的一些报错类型
|
||||
音·创 v3 用到的一些报错类型
|
||||
"""
|
||||
|
||||
"""
|
||||
@@ -106,10 +106,10 @@ class OuterlyParameterError(MusicreaterOuterlyError):
|
||||
|
||||
|
||||
class ZeroSpeedError(OuterlyParameterError, ZeroDivisionError):
|
||||
"""以0作为播放速度的错误"""
|
||||
"""以 0 作为播放速度的错误"""
|
||||
|
||||
def __init__(self, *args):
|
||||
"""以0作为播放速度的错误"""
|
||||
"""以 0 作为播放速度的错误"""
|
||||
super().__init__("播放速度为零:", *args)
|
||||
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ https://gitee.com/TriM-Organization/Musicreater/blob/master/LICENSE.md。
|
||||
# Bug retreat! Bug retreat!
|
||||
# Exceptions and errors are causing chaos
|
||||
# Words combine! Codes unite!
|
||||
# Hurry to call the programmer! Let's Go!
|
||||
# Hurry to call the Programmer! Let's Go!
|
||||
|
||||
import re
|
||||
|
||||
@@ -96,9 +96,10 @@ class MusiCreater:
|
||||
def _get_plugin_within_iousage(
|
||||
get_func: Callable[[Union[Path, str]], Generator[T_IOPlugin, None, None]],
|
||||
fpath: Path,
|
||||
plg_regdict: Dict[str, T_IOPlugin],
|
||||
plg_regdict: Mapping[str, T_IOPlugin],
|
||||
plg_id: Optional[str],
|
||||
) -> T_IOPlugin:
|
||||
"""这个函数是用于从指定的注册表项里面调取实例的,仅供下面这几个函数使用"""
|
||||
|
||||
__plugin: Optional[T_IOPlugin] = None
|
||||
if plg_id:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
存储 音·创 v3 内部数据使用的参数曲线
|
||||
音·创 v3 内部数据使用的参数曲线
|
||||
"""
|
||||
|
||||
"""
|
||||
@@ -26,12 +26,10 @@ Terms & Conditions: License.md in the root directory
|
||||
|
||||
from math import ceil
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional, Any, List, Tuple
|
||||
from typing import Optional, Any, List, Tuple, Callable
|
||||
from enum import Enum
|
||||
import bisect
|
||||
|
||||
from .types import FittingFunctionType
|
||||
|
||||
|
||||
def _evaluate_bezier_segment(
|
||||
t0: float,
|
||||
@@ -178,7 +176,7 @@ class Keyframe:
|
||||
value: float
|
||||
|
||||
# 函数插值模式
|
||||
out_interp: Optional[FittingFunctionType] = None
|
||||
out_interp: Optional[Callable[[float], float]] = None
|
||||
|
||||
# 贝塞尔模式
|
||||
in_tangent: Optional[Tuple[float, float]] = (
|
||||
@@ -215,7 +213,7 @@ class ParamCurve:
|
||||
base_line: float = 0.0
|
||||
"""基线/默认值"""
|
||||
|
||||
base_interpolation_function: FittingFunctionType
|
||||
base_interpolation_function: Callable[[float], float]
|
||||
"""默认(未指定区间时的)关键帧插值模式"""
|
||||
|
||||
boundary_behaviour: BoundaryBehaviour
|
||||
@@ -227,7 +225,7 @@ class ParamCurve:
|
||||
def __init__(
|
||||
self,
|
||||
base_value: float = 0.0,
|
||||
default_interpolation_function: FittingFunctionType = InterpolationMethod.linear,
|
||||
default_interpolation_function: Callable[[float], float] = InterpolationMethod.linear,
|
||||
boundary_mode: BoundaryBehaviour = BoundaryBehaviour.CONSTANT,
|
||||
):
|
||||
"""
|
||||
@@ -257,7 +255,7 @@ class ParamCurve:
|
||||
self,
|
||||
time: float,
|
||||
value: float,
|
||||
out_interp: Optional[FittingFunctionType] = None,
|
||||
out_interp: Optional[Callable[[float], float]] = None,
|
||||
in_tangent: Optional[Tuple[float, float]] = None,
|
||||
out_tangent: Optional[Tuple[float, float]] = None,
|
||||
use_bezier: bool = False,
|
||||
@@ -328,7 +326,7 @@ class ParamCurve:
|
||||
def update_key_interp(
|
||||
self,
|
||||
time: float,
|
||||
out_interp: Optional[FittingFunctionType] = None,
|
||||
out_interp: Optional[Callable[[float], float]] = None,
|
||||
in_tangent: Optional[Tuple[float, float]] = None,
|
||||
out_tangent: Optional[Tuple[float, float]] = None,
|
||||
use_bezier: bool = False,
|
||||
@@ -486,7 +484,7 @@ class ParamCurve:
|
||||
"""返回 (time, value) 列表。"""
|
||||
return [(k.time, k.value) for k in self._keys]
|
||||
|
||||
def set_default_interpolation_function(self, interp_func: FittingFunctionType):
|
||||
def set_default_interpolation_function(self, interp_func: Callable[[float], float]):
|
||||
"""设置默认插值函数。"""
|
||||
self.base_interpolation_function = interp_func
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
存储 音·创 v3 的插件接口与管理相关内容
|
||||
音·创 v3 的插件接口与管理相关内容
|
||||
"""
|
||||
|
||||
"""
|
||||
@@ -213,7 +213,7 @@ class PluginRegistry:
|
||||
|
||||
@staticmethod
|
||||
def _get_io_plugin_by_format(
|
||||
plugin_regdict: Dict[str, T_IOPlugin], fpath_or_format: Union[Path, str]
|
||||
plugin_regdict: Mapping[str, T_IOPlugin], fpath_or_format: Union[Path, str]
|
||||
) -> Generator[T_IOPlugin, None, None]:
|
||||
if isinstance(fpath_or_format, str):
|
||||
return (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
存储 音·创 v3 定义的一些数据类型,可以用于类型检查器
|
||||
音·创 v3 定义的一些数据类型,可以用于类型检查器
|
||||
"""
|
||||
|
||||
"""
|
||||
@@ -19,7 +19,3 @@ Terms & Conditions: License.md in the root directory
|
||||
|
||||
from typing import Callable, Dict, List, Literal, Mapping, Tuple, Union
|
||||
|
||||
FittingFunctionType = Callable[[float], float]
|
||||
"""
|
||||
拟合函数类型
|
||||
"""
|
||||
|
||||
36
TO-DO.md
36
TO-DO.md
@@ -6,31 +6,31 @@
|
||||
1. 使用 `.MCT` 作为项目文件的后缀,然后考虑一下格式是否和之前的 MusicSequence 兼容,如果兼容的话可以照旧用 `.MSQ`,如不的话,可以试试想一个新的后缀名作为数据文件后缀
|
||||
2. 要求数据文件支持完全流式读入
|
||||
|
||||
- 音轨静音处理
|
||||
- [] 音轨静音处理
|
||||
当前没有处理
|
||||
|
||||
- 优化音轨的存储方式
|
||||
- [] 优化音轨的存储方式
|
||||
当前是用列表,且每一次变动元素都要重新排序,这样消耗太大了,需要优化,改用最小堆形式(heapq)
|
||||
|
||||
- 移植 v2 功能到内置插件
|
||||
目前 v2 的功能有很多,都要移植到 v3。
|
||||
1. 导入 Midi 文件到全曲
|
||||
2. 导入 Midi 文件到指定轨道
|
||||
3. 导出到延迟播放器的结构文件(MCSTRUCTURE、BDX)
|
||||
4. 导出到延迟播放器的附加包
|
||||
5. 导出到积分板播放器的以上两种形式
|
||||
6. 导出到中继器播放器的以上两种形式
|
||||
7. 在 WebSocket 播放器中播放
|
||||
8. 导出到支持神羽资源包的以上 7 种形式
|
||||
9. 对于 Midi 歌词的实验性功能
|
||||
10. 对于 Java 版本适配的实验性功能
|
||||
11. 对于听感优化的实验性功能(插值、偏移)
|
||||
1. [x] 导入 Midi 文件到全曲
|
||||
2. [] 导入 Midi 文件到指定轨道
|
||||
3. [] 导出到延迟播放器的结构文件(MCSTRUCTURE、BDX)
|
||||
4. [] 导出到延迟播放器的附加包
|
||||
5. [] 导出到积分板播放器的以上两种形式
|
||||
6. [] 导出到中继器播放器的以上两种形式
|
||||
7. [] 在 WebSocket 播放器中播放
|
||||
8. [] 导出到支持神羽资源包的以上 7 种形式
|
||||
9. [] 对于 Midi 歌词的实验性功能
|
||||
10. [] 对于 Java 版本适配的实验性功能
|
||||
11. [] 对于听感优化的实验性功能(插值、偏移)
|
||||
|
||||
- 测试参数曲线的功能
|
||||
- [] 测试参数曲线的功能
|
||||
|
||||
- 支持导出音符盒构成的音乐
|
||||
- [] 支持导出音符盒构成的音乐
|
||||
|
||||
- 支持导出成 schematic 结构
|
||||
- [] 支持导出成 schematic 结构
|
||||
|
||||
## 讨论
|
||||
|
||||
@@ -41,8 +41,8 @@
|
||||
|
||||
引入了插件惟一识别码之后当然是采用 `Dict[插件唯一识别码, 插件对象]` 来存储插件了~之前插件名称的内容是我想得太浅了,我写完所有代码之后才想到插件名称是中文还带空格的任意字符串……
|
||||
|
||||
2. 服务插件到底该怎么写?总不能留着一个 PluginType.SERVICE 的插件一直空在那里吧……
|
||||
2. [] 服务插件到底该怎么写?总不能留着一个 PluginType.SERVICE 的插件一直空在那里吧……
|
||||
|
||||
3. 插件依赖性的优化。目前没有处理各个插件依赖关系的问题,如果插件之间彼此依赖要怎么做?
|
||||
3. [] 插件依赖性的优化。目前没有处理各个插件依赖关系的问题,如果插件之间彼此依赖要怎么做?
|
||||
我的想法是,这个依赖的处理由调用端来完成。比如我们的 伶伦工作站 是以 音·创 为核心的一个可视化数字音频工作站。
|
||||
那么应该由伶伦来处理依赖关系并加载之。
|
||||
|
||||
237
docs/异常继承关系.mmd
Normal file
237
docs/异常继承关系.mmd
Normal file
@@ -0,0 +1,237 @@
|
||||
|
||||
classDiagram
|
||||
|
||||
direction LR
|
||||
|
||||
class Exception{
|
||||
Python 内置基类
|
||||
}
|
||||
|
||||
class MusicreaterBaseException {
|
||||
"[音·创] - ..."
|
||||
所有音·创 v3 错误的基类
|
||||
}
|
||||
|
||||
class MusicreaterInnerlyError {
|
||||
"内部错误 - ..."
|
||||
面向开发者的内部错误
|
||||
}
|
||||
|
||||
class MusicreaterOuterlyError {
|
||||
"外部错误 - ..."
|
||||
面向用户的外部错误
|
||||
}
|
||||
|
||||
class InnerlyParameterError {
|
||||
"内部传参错误 - ..."
|
||||
内部参数相关错误
|
||||
}
|
||||
|
||||
class OuterlyParameterError {
|
||||
"参数错误 - ..."
|
||||
外部参数相关错误
|
||||
}
|
||||
|
||||
class ParameterTypeError {
|
||||
"参数类型错误:..."
|
||||
继承自 InnerlyParameterError 和 TypeError
|
||||
}
|
||||
|
||||
class ParameterValueError {
|
||||
"参数数值错误:..."
|
||||
继承自 InnerlyParameterError 和 ValueError
|
||||
}
|
||||
|
||||
class PluginNotSpecifiedError {
|
||||
"未指定插件:..."
|
||||
继承自 InnerlyParameterError 和 LookupError
|
||||
}
|
||||
|
||||
class ZeroSpeedError {
|
||||
"播放速度为零:..."
|
||||
继承自 OuterlyParameterError 和 ZeroDivisionError
|
||||
}
|
||||
|
||||
class IllegalMinimumVolumeError {
|
||||
"最小播放音量超出范围:..."
|
||||
继承自 OuterlyParameterError 和 ValueError
|
||||
}
|
||||
|
||||
class FileFormatNotSupportedError {
|
||||
"不支持的文件格式:..."
|
||||
继承自 MusicreaterOuterlyError
|
||||
}
|
||||
|
||||
class NoteBinaryDecodeError {
|
||||
"解码音乐存储二进制数据时出现问题 - ..."
|
||||
继承自 MusicreaterOuterlyError
|
||||
}
|
||||
|
||||
class SingleNoteDecodeError {
|
||||
"音符解码出错:..."
|
||||
继承自 NoteBinaryDecodeError
|
||||
}
|
||||
|
||||
class NoteBinaryFileTypeError {
|
||||
"无法识别音乐存储文件对应的类型:..."
|
||||
继承自 NoteBinaryDecodeError
|
||||
}
|
||||
|
||||
class NoteBinaryFileVerificationFailed {
|
||||
"音乐存储文件校验失败:..."
|
||||
继承自 NoteBinaryDecodeError
|
||||
}
|
||||
|
||||
class PluginDefineError {
|
||||
"插件内部错误 - ..."
|
||||
插件定义相关的内部错误
|
||||
}
|
||||
|
||||
class PluginInstanceNotFoundError {
|
||||
"插件实例未找到:..."
|
||||
继承自 PluginDefineError 和 LookupError
|
||||
}
|
||||
|
||||
class PluginAttributeNotFoundError {
|
||||
"插件类的必要属性不存在:..."
|
||||
继承自 PluginDefineError 和 AttributeError
|
||||
}
|
||||
|
||||
class PluginMetainfoError {
|
||||
"插件元信息定义错误 - ..."
|
||||
插件元信息相关错误
|
||||
}
|
||||
|
||||
class PluginMetainfoTypeError {
|
||||
"插件元信息类型错误:..."
|
||||
继承自 PluginMetainfoError 和 TypeError
|
||||
}
|
||||
|
||||
class PluginMetainfoValueError {
|
||||
"插件元信息数值错误:..."
|
||||
继承自 PluginMetainfoError 和 ValueError
|
||||
}
|
||||
|
||||
class PluginMetainfoNotFoundError {
|
||||
"插件元信息未定义:..."
|
||||
继承自 PluginMetainfoError 和 PluginAttributeNotFoundError
|
||||
}
|
||||
|
||||
class PluginLoadError {
|
||||
"插件加载错误 - ..."
|
||||
插件加载相关的外部错误
|
||||
}
|
||||
|
||||
class PluginNotFoundError {
|
||||
"插件未找到:..."
|
||||
继承自 PluginLoadError
|
||||
}
|
||||
|
||||
class PluginRegisteredError {
|
||||
"插件重复注册:..."
|
||||
继承自 PluginLoadError
|
||||
}
|
||||
|
||||
class PluginConfigRelatedError {
|
||||
"插件配置相关错误 - ..."
|
||||
插件配置相关错误基类
|
||||
}
|
||||
|
||||
class PluginConfigLoadError {
|
||||
"插件配置文件加载错误:..."
|
||||
继承自 PluginLoadError、PluginConfigRelatedError
|
||||
}
|
||||
|
||||
class PluginConfigDumpError {
|
||||
"插件配置文件保存错误:..."
|
||||
继承自 PluginConfigRelatedError
|
||||
}
|
||||
%% 高亮定义
|
||||
|
||||
class ParameterTypeError ::: highlight
|
||||
class ParameterValueError ::: highlight
|
||||
class PluginNotSpecifiedError ::: highlight
|
||||
class ZeroSpeedError ::: highlight
|
||||
class IllegalMinimumVolumeError ::: highlight
|
||||
class FileFormatNotSupportedError ::: highlight
|
||||
class SingleNoteDecodeError ::: highlight
|
||||
class NoteBinaryFileTypeError ::: highlight
|
||||
class NoteBinaryFileVerificationFailed ::: highlight
|
||||
class PluginInstanceNotFoundError ::: highlight
|
||||
class PluginAttributeNotFoundError ::: highlight
|
||||
class PluginMetainfoTypeError ::: highlight
|
||||
class PluginMetainfoValueError ::: highlight
|
||||
class PluginMetainfoNotFoundError ::: highlight
|
||||
class PluginNotFoundError ::: highlight
|
||||
class PluginRegisteredError ::: highlight
|
||||
class PluginConfigLoadError ::: highlight
|
||||
class PluginConfigDumpError ::: highlight
|
||||
|
||||
%% 定义高亮样式
|
||||
classDef highlight fill:,stroke-width:5px
|
||||
|
||||
%% 继承关系(箭头从子类指向父类)
|
||||
Exception <|-- MusicreaterBaseException
|
||||
Exception <|-- TypeError
|
||||
Exception <|-- ValueError
|
||||
Exception <|-- LookupError
|
||||
Exception <|-- AttributeError
|
||||
Exception <|-- ZeroDivisionError
|
||||
MusicreaterBaseException <|-- MusicreaterInnerlyError
|
||||
MusicreaterBaseException <|-- MusicreaterOuterlyError
|
||||
|
||||
MusicreaterInnerlyError <|-- InnerlyParameterError
|
||||
MusicreaterOuterlyError <|-- OuterlyParameterError
|
||||
|
||||
InnerlyParameterError <|-- ParameterTypeError
|
||||
TypeError <|-- ParameterTypeError
|
||||
|
||||
InnerlyParameterError <|-- ParameterValueError
|
||||
ValueError <|-- ParameterValueError
|
||||
|
||||
InnerlyParameterError <|-- PluginNotSpecifiedError
|
||||
LookupError <|-- PluginNotSpecifiedError
|
||||
|
||||
OuterlyParameterError <|-- ZeroSpeedError
|
||||
ZeroDivisionError <|-- ZeroSpeedError
|
||||
|
||||
OuterlyParameterError <|-- IllegalMinimumVolumeError
|
||||
ValueError <|-- IllegalMinimumVolumeError
|
||||
|
||||
MusicreaterOuterlyError <|-- FileFormatNotSupportedError
|
||||
MusicreaterOuterlyError <|-- NoteBinaryDecodeError
|
||||
|
||||
NoteBinaryDecodeError <|-- SingleNoteDecodeError
|
||||
NoteBinaryDecodeError <|-- NoteBinaryFileTypeError
|
||||
NoteBinaryDecodeError <|-- NoteBinaryFileVerificationFailed
|
||||
|
||||
MusicreaterInnerlyError <|-- PluginDefineError
|
||||
|
||||
PluginDefineError <|-- PluginInstanceNotFoundError
|
||||
LookupError <|-- PluginInstanceNotFoundError
|
||||
|
||||
PluginDefineError <|-- PluginAttributeNotFoundError
|
||||
AttributeError <|-- PluginAttributeNotFoundError
|
||||
|
||||
PluginDefineError <|-- PluginMetainfoError
|
||||
|
||||
PluginMetainfoError <|-- PluginMetainfoTypeError
|
||||
TypeError <|-- PluginMetainfoTypeError
|
||||
|
||||
PluginMetainfoError <|-- PluginMetainfoValueError
|
||||
ValueError <|-- PluginMetainfoValueError
|
||||
|
||||
PluginMetainfoError <|-- PluginMetainfoNotFoundError
|
||||
PluginAttributeNotFoundError <|-- PluginMetainfoNotFoundError
|
||||
|
||||
MusicreaterOuterlyError <|-- PluginLoadError
|
||||
|
||||
PluginLoadError <|-- PluginNotFoundError
|
||||
PluginLoadError <|-- PluginRegisteredError
|
||||
|
||||
MusicreaterOuterlyError <|-- PluginConfigRelatedError
|
||||
|
||||
PluginLoadError <|-- PluginConfigLoadError
|
||||
PluginConfigRelatedError <|-- PluginConfigLoadError
|
||||
|
||||
PluginConfigRelatedError <|-- PluginConfigDumpError
|
||||
67
docs/异常继承关系.svg
Normal file
67
docs/异常继承关系.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 652 KiB |
125
examples/doc_importdata_plugin.md
Normal file
125
examples/doc_importdata_plugin.md
Normal file
@@ -0,0 +1,125 @@
|
||||
|
||||
|
||||
# 示例插件:导入音符数据
|
||||
|
||||
> 版权所有 © 2026 金羿
|
||||
> Copyright © 2026 Eilles
|
||||
|
||||
睿乐组织 开发交流群 [861684859](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=fxNYIX_zKMgaO8X6K7pP7tHtLB7JRvdX&noverify=0&group_code=861684859)
|
||||
Email [TriM-Organization@hotmail.com](mailto:TriM-Organization@hotmail.com)
|
||||
|
||||
```license
|
||||
本示例模块开放授权,同时,本教程文件已开放至公共领域。
|
||||
请注意:
|
||||
若是对本文件的直接转载(在形式上没有修改、增删、添加注释,或单纯修改排版、翻译、录屏、截图)
|
||||
则该使用者需要在转载所及之处,明确在转载的内容开头标注本文之原始著作权人
|
||||
在当前文件下,该原始著作权人为金羿(Eilles)
|
||||
如果是对本文进行了一定程度上的修改和补充、或者以不同方式演绎本文件(如制成视频教程等)
|
||||
则无需标注原作者,允许该使用者自行署名
|
||||
|
||||
本声明仅限于包含此声明的本文件,本声明与项目内其他文件无关。
|
||||
```
|
||||
|
||||
## 新建文件夹 · 基础模块知识
|
||||
|
||||
|
||||
首先,一个 **音·创 v3** 的插件应当存储于一个 Python 模块之中,也就是插件存在于可以被 import 语句引入的 module 中。
|
||||
|
||||
这就意味着,承载插件的模块本质上可以是多个 Python 的 `.py` 文件组成的,带有 `__init__.py` 的一个文件夹;
|
||||
或者是一个简单的 `.py` 文件。
|
||||
|
||||
我们有这种共识:你已经知道了模块的相关知识,我后面无需赘述插件和模块的区别。
|
||||
|
||||
## 开始编写插件 · 插件基础
|
||||
|
||||
|
||||
首先导入插件所需的类。
|
||||
|
||||
在这里我们是一个用来导入数据的插件。
|
||||
|
||||
所以就需要导入 `MusicInputPluginBase` 类和 `music_input_plugin` 函数。
|
||||
|
||||
同时,`PluginMetaInformation` 类和 `PluginTypes` 类也必须导入,这是插件的元信息所需要的。
|
||||
|
||||
```python
|
||||
from Musicreater.plugins import (
|
||||
music_input_plugin,
|
||||
PluginMetaInformation,
|
||||
PluginTypes,
|
||||
MusicInputPluginBase,
|
||||
)
|
||||
```
|
||||
|
||||
如果插件需要配置,那么请再导入 `PluginConfig` 类,并从此继承一个类,且须用 dataclass 装饰器来注册之。
|
||||
_对于这个类的使用方式,可以阅读 dataclass 的官方文档_
|
||||
|
||||
```python
|
||||
from Musicreater.plugins import PluginConfig
|
||||
from dataclasses import dataclass
|
||||
@dataclass
|
||||
class ExampleImportConfig(PluginConfig):
|
||||
example_config_item3: bool
|
||||
example_config_item1: str = "example_config_item"
|
||||
example_config_item2: int = 0
|
||||
```
|
||||
|
||||
## 编写插件 · 开始
|
||||
|
||||
接着我们来制作一个插件。
|
||||
|
||||
首先,一个 **音·创 v3** 的插件应当是一个继承自我们已经准备好的插件基类的**类**(缩句:插件是类);
|
||||
在 **音·创 v3** 中,任何对音乐的操作,包括导入、导出、处理,都分为对 **整首曲目** 的操作和对 **单个音轨** 的操作。
|
||||
|
||||
我们的样例是一个对**整首曲目**进行**导入操作**的插件,因此需要继承 `MusicInputPluginBase` 类。
|
||||
插件类的类名称不得以 `Base` 结尾,因为咱写的是插件,不是插件基类。
|
||||
|
||||
在插件的类的开头,需要用插件注册装饰函数来对插件类装饰。
|
||||
```python
|
||||
@music_input_plugin("example_import_plugin")
|
||||
class xxx:
|
||||
...
|
||||
```
|
||||
我们这里对应插件类型的注册器是 `music_input_plugin` 函数。
|
||||
在注册器函数后的参数,是这个插件的惟一识别码。不应与其他插件混淆。
|
||||
通常可以是这个插件的功能描述、或者就是插件名。
|
||||
|
||||
接着编写这个插件,也即是此类。
|
||||
每个插件的类必须包含一个用于指定插件元信息的 `metainfo` 属性。
|
||||
如果插件是导入数据或者导出数据的插件,则必须包含一个 `supported_formats` 属性,用以声明插件所支持的数据格式。
|
||||
|
||||
用于导入的插件类必须包含一个 `loadbytes` 方法,用于从字节流中导入数据。可选是否单独实现 `load` 方法,如果不单独实现,则在调用时,会直接通过打开文件后使用 `loadbytes` 的方式实现。
|
||||
|
||||
```python
|
||||
# 注册插件
|
||||
@music_input_plugin("something_convert_to_music")
|
||||
# 继承自对应类型的插件基类
|
||||
class ExampleImportPlugin(MusicInputPluginBase):
|
||||
|
||||
# 插件元信息定义
|
||||
metainfo = PluginMetaInformation(
|
||||
name="示例导入插件", # 插件名称
|
||||
author="金羿", # 插件作者
|
||||
description="这是一个示例导入插件", # 插件描述
|
||||
version=(0, 0, 1), # 插件版本
|
||||
type=PluginTypes.FUNCTION_MUSIC_IMPORT, # 插件类型
|
||||
license="The Unlicense", # 插件许可证
|
||||
dependencies=("something_convertion_library") # 插件对于其他插件的依赖项
|
||||
)
|
||||
|
||||
# 导入导出插件支持的数据格式,大小写皆可
|
||||
supported_formats = ("EXP", "example_format")
|
||||
|
||||
# 定义 loadbytes 方法,从字节流中导入数据
|
||||
def loadbytes(
|
||||
self, bytes_buffer_in: BinaryIO, config: ExampleImportConfig
|
||||
) -> "SingleMusic":
|
||||
...
|
||||
|
||||
# 插件可选地定义 load 方法,从文件导入数据
|
||||
def load(
|
||||
self, file_path: Path, config: ExampleImportConfig
|
||||
) -> "SingleMusic":
|
||||
...
|
||||
```
|
||||
|
||||
至此,一个插件的编写已经完成。
|
||||
70
examples/exp_importdata_plugin.py
Normal file
70
examples/exp_importdata_plugin.py
Normal file
@@ -0,0 +1,70 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
示例插件:导入音符数据
|
||||
"""
|
||||
|
||||
"""
|
||||
版权所有 © 2026 金羿
|
||||
Copyright © 2026 Eilles
|
||||
"""
|
||||
|
||||
# 睿乐组织 开发交流群 861684859
|
||||
# Email TriM-Organization@hotmail.com
|
||||
|
||||
"""
|
||||
本示例模块开放授权,本文件已开放至公共领域。
|
||||
请注意:
|
||||
若是对本文件的直接转载(在形式上没有修改、增删、添加注释,或单纯修改排版、翻译、录屏、截图)
|
||||
则该使用者需要在转载所及之处,明确在转载的内容开头标注本文之原始著作权人
|
||||
在当前文件下,该原始著作权人为金羿(Eilles)
|
||||
如果是对本文进行了一定程度上的修改和补充、或者以不同方式演绎本文件(如制成视频教程等)
|
||||
则无需标注原作者,允许该使用者自行署名
|
||||
|
||||
本声明仅限于包含此声明的本文件,本声明与项目内其他文件无关。
|
||||
"""
|
||||
|
||||
from typing import BinaryIO
|
||||
from pathlib import Path
|
||||
from dataclasses import dataclass
|
||||
|
||||
from Musicreater import SingleMusic
|
||||
from Musicreater.plugins import (
|
||||
music_input_plugin,
|
||||
PluginConfig,
|
||||
PluginMetaInformation,
|
||||
PluginTypes,
|
||||
MusicInputPluginBase,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExampleImportConfig(PluginConfig):
|
||||
example_config_item3: bool
|
||||
example_config_item1: str = "example_config_item"
|
||||
example_config_item2: int = 0
|
||||
|
||||
|
||||
@music_input_plugin("something_convert_to_music")
|
||||
class ExampleImportPlugin(MusicInputPluginBase):
|
||||
metainfo = PluginMetaInformation(
|
||||
name="示例导入插件",
|
||||
author="金羿",
|
||||
description="这是一个示例导入插件",
|
||||
version=(0, 0, 1),
|
||||
type=PluginTypes.FUNCTION_MUSIC_IMPORT,
|
||||
license="The Unlicense",
|
||||
dependencies=("something_convertion_library")
|
||||
)
|
||||
|
||||
supported_formats = ("EXP", "example_format")
|
||||
|
||||
def loadbytes(
|
||||
self, bytes_buffer_in: BinaryIO, config: ExampleImportConfig
|
||||
) -> "SingleMusic":
|
||||
return SingleMusic()
|
||||
|
||||
def load(
|
||||
self, file_path: Path, config: ExampleImportConfig
|
||||
) -> "SingleMusic":
|
||||
return SingleMusic()
|
||||
@@ -182,10 +182,10 @@ class FutureMidiConvertKamiRES(MidiConvert):
|
||||
raise ZeroSpeedError("播放速度为 0 ,其需要(0,1]范围内的实数。")
|
||||
|
||||
# 一个midi中仅有16个通道 我们通过通道来识别而不是音轨
|
||||
midi_channels: MineNoteChannelType = empty_midi_channels(default_staff=[])
|
||||
midi_channels: MineNoteChannelType = enumerated_stuff_copy(staff=[])
|
||||
|
||||
channel_controler: Dict[int, Dict[str, int]] = empty_midi_channels(
|
||||
default_staff={
|
||||
channel_controler: Dict[int, Dict[str, int]] = enumerated_stuff_copy(
|
||||
staff={
|
||||
MIDI_PROGRAM: default_program_value,
|
||||
MIDI_VOLUME: default_volume_value,
|
||||
MIDI_PAN: 64,
|
||||
@@ -205,7 +205,7 @@ class FutureMidiConvertKamiRES(MidiConvert):
|
||||
int,
|
||||
]
|
||||
],
|
||||
] = empty_midi_channels(default_staff=[])
|
||||
] = enumerated_stuff_copy(staff=[])
|
||||
note_queue_B: Dict[
|
||||
int,
|
||||
List[
|
||||
@@ -214,7 +214,7 @@ class FutureMidiConvertKamiRES(MidiConvert):
|
||||
int,
|
||||
]
|
||||
],
|
||||
] = empty_midi_channels(default_staff=[])
|
||||
] = enumerated_stuff_copy(staff=[])
|
||||
|
||||
# 直接使用mido.midifiles.tracks.merge_tracks转为单轨
|
||||
# 采用的时遍历信息思路
|
||||
@@ -1042,7 +1042,7 @@ class FutureMidiConvertM5(MidiConvert):
|
||||
# )
|
||||
|
||||
# 一个midi中仅有16个通道 我们通过通道来识别而不是音轨
|
||||
midi_channels: ChannelType = empty_midi_channels()
|
||||
midi_channels: ChannelType = enumerated_stuff_copy()
|
||||
tempo = 500000
|
||||
|
||||
# 我们来用通道统计音乐信息
|
||||
@@ -1052,7 +1052,7 @@ class FutureMidiConvertM5(MidiConvert):
|
||||
if not track:
|
||||
continue
|
||||
|
||||
note_queue = empty_midi_channels(default_staff=[])
|
||||
note_queue = enumerated_stuff_copy(staff=[])
|
||||
|
||||
for msg in track:
|
||||
if msg.time != 0:
|
||||
|
||||
@@ -34,14 +34,6 @@ class MSCTBaseException(Exception):
|
||||
raise self
|
||||
|
||||
|
||||
class MidiFormatException(MSCTBaseException):
|
||||
"""音·创 的所有MIDI格式错误均继承于此"""
|
||||
|
||||
def __init__(self, *args):
|
||||
"""音·创 的所有MIDI格式错误均继承于此"""
|
||||
super().__init__("MIDI 格式错误", *args)
|
||||
|
||||
|
||||
class MidiDestroyedError(MSCTBaseException):
|
||||
"""Midi文件损坏"""
|
||||
|
||||
@@ -82,84 +74,3 @@ class CommandFormatError(MSCTBaseException, RuntimeError):
|
||||
# 那么这两个音符的音长无法判断。这是个好问题,但是不是我现在能解决的,也不是我们现在想解决的问题
|
||||
|
||||
|
||||
class NotDefineTempoError(MidiFormatException):
|
||||
"""没有Tempo设定导致时间无法计算的错误"""
|
||||
|
||||
def __init__(self, *args):
|
||||
"""没有Tempo设定导致时间无法计算的错误"""
|
||||
super().__init__("在曲目开始时没有声明 Tempo(未指定拍长)", *args)
|
||||
|
||||
|
||||
class ChannelOverFlowError(MidiFormatException):
|
||||
"""一个midi中含有过多的通道"""
|
||||
|
||||
def __init__(self, max_channel=16, *args):
|
||||
"""一个midi中含有过多的通道"""
|
||||
super().__init__("含有过多的通道(数量应≤{})".format(max_channel), *args)
|
||||
|
||||
|
||||
class NotDefineProgramError(MidiFormatException):
|
||||
"""没有Program设定导致没有乐器可以选择的错误"""
|
||||
|
||||
def __init__(self, *args):
|
||||
"""没有Program设定导致没有乐器可以选择的错误"""
|
||||
super().__init__("未指定演奏乐器", *args)
|
||||
|
||||
|
||||
class NoteOnOffMismatchError(MidiFormatException):
|
||||
"""音符开音和停止不匹配的错误"""
|
||||
|
||||
def __init__(self, *args):
|
||||
"""音符开音和停止不匹配的错误"""
|
||||
super().__init__("音符不匹配", *args)
|
||||
|
||||
|
||||
class LyricMismatchError(MSCTBaseException):
|
||||
"""歌词匹配解析错误"""
|
||||
|
||||
def __init__(self, *args):
|
||||
"""有可能产生了错误的歌词解析"""
|
||||
super().__init__("歌词解析错误", *args)
|
||||
|
||||
# 已重构
|
||||
class ZeroSpeedError(MSCTBaseException, ZeroDivisionError):
|
||||
"""以0作为播放速度的错误"""
|
||||
|
||||
def __init__(self, *args):
|
||||
"""以0作为播放速度的错误"""
|
||||
super().__init__("播放速度为零", *args)
|
||||
|
||||
# 已重构
|
||||
class IllegalMinimumVolumeError(MSCTBaseException, ValueError):
|
||||
"""最小播放音量有误的错误"""
|
||||
|
||||
def __init__(self, *args):
|
||||
"""最小播放音量错误"""
|
||||
super().__init__("最小播放音量超出范围", *args)
|
||||
|
||||
|
||||
# 已重构
|
||||
class MusicSequenceDecodeError(MSCTBaseException):
|
||||
"""音乐序列解码错误"""
|
||||
|
||||
def __init__(self, *args):
|
||||
"""音乐序列无法正确解码的错误"""
|
||||
super().__init__("解码音符序列文件时出现问题", *args)
|
||||
|
||||
|
||||
# 已重构
|
||||
class MusicSequenceTypeError(MSCTBaseException):
|
||||
"""音乐序列类型错误"""
|
||||
|
||||
def __init__(self, *args):
|
||||
"""无法识别音符序列字节码的类型"""
|
||||
super().__init__("错误的音符序列字节类型", *args)
|
||||
|
||||
|
||||
# 已重构
|
||||
class MusicSequenceVerificationFailed(MusicSequenceDecodeError):
|
||||
"""音乐序列校验失败"""
|
||||
|
||||
def __init__(self, *args):
|
||||
"""音符序列文件与其校验值不一致"""
|
||||
super().__init__("音符序列文件校验失败", *args)
|
||||
|
||||
@@ -170,7 +170,7 @@ class MusicSequence:
|
||||
pitched_note_referance_table: MidiInstrumentTableType = MM_TOUCH_PITCHED_INSTRUMENT_TABLE,
|
||||
percussion_note_referance_table: MidiInstrumentTableType = MM_TOUCH_PERCUSSION_INSTRUMENT_TABLE,
|
||||
minimum_vol: float = 0.1,
|
||||
volume_processing_function: FittingFunctionType = velocity_2_distance_natural,
|
||||
volume_processing_function: FittingFunctionType = volume_2_distance_natural,
|
||||
panning_processing_function: FittingFunctionType = panning_2_rotation_linear,
|
||||
deviation: float = 0,
|
||||
note_referance_table_replacement: Dict[str, str] = {},
|
||||
@@ -273,7 +273,7 @@ class MusicSequence:
|
||||
8 : (stt_index := 8 + (group_1 >> 10))
|
||||
].decode("GB18030")
|
||||
|
||||
channels_: MineNoteChannelType = empty_midi_channels(default_staff=[])
|
||||
channels_: MineNoteChannelType = enumerated_stuff_copy(staff=[])
|
||||
total_note_count = 0
|
||||
if verify:
|
||||
_header_index = stt_index
|
||||
@@ -415,7 +415,7 @@ class MusicSequence:
|
||||
_t6_buffer = _t2_buffer = 0
|
||||
|
||||
_channel_inst_chart: Dict[str, Dict[str, int]] = {}
|
||||
channels_: MineNoteChannelType = empty_midi_channels(default_staff=[])
|
||||
channels_: MineNoteChannelType = enumerated_stuff_copy(staff=[])
|
||||
|
||||
for i in range(total_note_count):
|
||||
if verify:
|
||||
@@ -525,7 +525,7 @@ class MusicSequence:
|
||||
music_name_ = bytes_buffer_in[
|
||||
8 : (stt_index := 8 + (group_1 >> 10))
|
||||
].decode("GB18030")
|
||||
channels_: MineNoteChannelType = empty_midi_channels(default_staff=[])
|
||||
channels_: MineNoteChannelType = enumerated_stuff_copy(staff=[])
|
||||
for channel_index in channels_.keys():
|
||||
for i in range(
|
||||
int.from_bytes(
|
||||
@@ -568,7 +568,7 @@ class MusicSequence:
|
||||
music_name_ = bytes_buffer_in[
|
||||
8 : (stt_index := 8 + (group_1 >> 10))
|
||||
].decode("utf-8")
|
||||
channels_: MineNoteChannelType = empty_midi_channels(default_staff=[])
|
||||
channels_: MineNoteChannelType = enumerated_stuff_copy(staff=[])
|
||||
for channel_index in channels_.keys():
|
||||
for i in range(
|
||||
int.from_bytes(
|
||||
@@ -820,7 +820,7 @@ class MusicSequence:
|
||||
default_tempo_value: int = mido.midifiles.midifiles.DEFAULT_TEMPO,
|
||||
pitched_note_rtable: MidiInstrumentTableType = MM_TOUCH_PITCHED_INSTRUMENT_TABLE,
|
||||
percussion_note_rtable: MidiInstrumentTableType = MM_TOUCH_PERCUSSION_INSTRUMENT_TABLE,
|
||||
vol_processing_function: FittingFunctionType = velocity_2_distance_natural,
|
||||
vol_processing_function: FittingFunctionType = volume_2_distance_natural,
|
||||
pan_processing_function: FittingFunctionType = panning_2_rotation_trigonometric,
|
||||
note_rtable_replacement: Dict[str, str] = {},
|
||||
) -> Tuple[MineNoteChannelType, int, Dict[str, int]]:
|
||||
@@ -860,10 +860,10 @@ class MusicSequence:
|
||||
raise ZeroSpeedError("播放速度不得为零,应为 (0,1] 范围内的实数。")
|
||||
|
||||
# 一个midi中仅有16个通道 我们通过通道来识别而不是音轨
|
||||
midi_channels: MineNoteChannelType = empty_midi_channels(default_staff=[])
|
||||
midi_channels: MineNoteChannelType = enumerated_stuff_copy(staff=[])
|
||||
|
||||
channel_controler: Dict[int, Dict[str, int]] = empty_midi_channels(
|
||||
default_staff={
|
||||
channel_controler: Dict[int, Dict[str, int]] = enumerated_stuff_copy(
|
||||
staff={
|
||||
MIDI_PROGRAM: default_program_value,
|
||||
MIDI_VOLUME: default_volume_value,
|
||||
MIDI_PAN: 64,
|
||||
@@ -883,7 +883,7 @@ class MusicSequence:
|
||||
int,
|
||||
]
|
||||
],
|
||||
] = empty_midi_channels(default_staff=[])
|
||||
] = enumerated_stuff_copy(staff=[])
|
||||
note_queue_B: Dict[
|
||||
int,
|
||||
List[
|
||||
@@ -892,7 +892,7 @@ class MusicSequence:
|
||||
int,
|
||||
]
|
||||
],
|
||||
] = empty_midi_channels(default_staff=[])
|
||||
] = enumerated_stuff_copy(staff=[])
|
||||
|
||||
lyric_cache: List[Tuple[int, str]] = []
|
||||
|
||||
@@ -1099,7 +1099,7 @@ class MidiConvert(MusicSequence):
|
||||
percussion_note_rtable: MidiInstrumentTableType = MM_TOUCH_PERCUSSION_INSTRUMENT_TABLE,
|
||||
enable_old_exe_format: bool = False,
|
||||
minimum_volume: float = 0.1,
|
||||
vol_processing_function: FittingFunctionType = velocity_2_distance_natural,
|
||||
vol_processing_function: FittingFunctionType = volume_2_distance_natural,
|
||||
pan_processing_function: FittingFunctionType = panning_2_rotation_trigonometric,
|
||||
pitch_deviation: float = 0,
|
||||
note_rtable_replacement: Dict[str, str] = {},
|
||||
@@ -1182,7 +1182,7 @@ class MidiConvert(MusicSequence):
|
||||
percussion_note_table: MidiInstrumentTableType = MM_TOUCH_PERCUSSION_INSTRUMENT_TABLE,
|
||||
old_exe_format: bool = False,
|
||||
min_volume: float = 0.1,
|
||||
vol_processing_func: FittingFunctionType = velocity_2_distance_natural,
|
||||
vol_processing_func: FittingFunctionType = volume_2_distance_natural,
|
||||
pan_processing_func: FittingFunctionType = panning_2_rotation_linear,
|
||||
music_pitch_deviation: float = 0,
|
||||
note_table_replacement: Dict[str, str] = {},
|
||||
|
||||
@@ -39,31 +39,15 @@ from Musicreater.constants import (
|
||||
MM_INSTRUMENT_DEVIATION_TABLE,
|
||||
MM_INSTRUMENT_RANGE_TABLE,
|
||||
)
|
||||
from .old_exceptions import MusicSequenceDecodeError
|
||||
from Musicreater.exceptions import SingleNoteDecodeError
|
||||
from Musicreater._utils import enumerated_stuffcopy_dictionary
|
||||
|
||||
from Musicreater.builtin_plugins.midi_read.utils import midi_inst_to_mc_sound
|
||||
|
||||
from .subclass import MineNote, mctick2timestr, SingleNoteBox
|
||||
from .old_types import MidiInstrumentTableType, MineNoteChannelType, FittingFunctionType
|
||||
|
||||
|
||||
def empty_midi_channels(
|
||||
channel_count: int = 17, default_staff: Any = {}
|
||||
) -> Dict[int, Any]:
|
||||
"""
|
||||
空MIDI通道字典
|
||||
"""
|
||||
|
||||
return dict(
|
||||
(
|
||||
i,
|
||||
(
|
||||
default_staff.copy()
|
||||
if isinstance(default_staff, (dict, list))
|
||||
else default_staff
|
||||
),
|
||||
) # 这告诉我们,你不能忽略任何一个复制的序列,因为它真的,我哭死,折磨我一整天,全在这个bug上了
|
||||
for i in range(channel_count)
|
||||
)
|
||||
|
||||
|
||||
def inst_to_sould_with_deviation(
|
||||
instrumentID: int,
|
||||
reference_table: MidiInstrumentTableType,
|
||||
@@ -99,34 +83,6 @@ def inst_to_sould_with_deviation(
|
||||
)
|
||||
|
||||
|
||||
def midi_inst_to_mc_sound(
|
||||
instrumentID: int,
|
||||
reference_table: MidiInstrumentTableType,
|
||||
default_instrument: str = "note.flute",
|
||||
) -> str:
|
||||
"""
|
||||
返回midi的乐器ID对应的我的世界乐器名
|
||||
|
||||
Parameters
|
||||
----------
|
||||
instrumentID: int
|
||||
midi的乐器ID
|
||||
reference_table: Dict[int, Tuple[str, int]]
|
||||
转换乐器参照表
|
||||
default_instrument: str
|
||||
查无此乐器时的替换乐器
|
||||
|
||||
Returns
|
||||
-------
|
||||
str我的世界乐器名
|
||||
"""
|
||||
return reference_table.get(
|
||||
instrumentID,
|
||||
default_instrument,
|
||||
)
|
||||
|
||||
|
||||
|
||||
def minenote_to_command_parameters(
|
||||
mine_note: MineNote,
|
||||
pitch_deviation: float = 0,
|
||||
@@ -175,85 +131,6 @@ def minenote_to_command_parameters(
|
||||
)
|
||||
|
||||
|
||||
def midi_msgs_to_minenote(
|
||||
inst_: int, # 乐器编号
|
||||
note_: int,
|
||||
percussive_: bool, # 是否作为打击乐器启用
|
||||
volume_: int,
|
||||
velocity_: int,
|
||||
panning_: int,
|
||||
start_time_: int,
|
||||
duration_: int,
|
||||
play_speed: float,
|
||||
midi_reference_table: MidiInstrumentTableType,
|
||||
volume_processing_method_: FittingFunctionType,
|
||||
panning_processing_method_: FittingFunctionType,
|
||||
note_table_replacement: Dict[str, str] = {},
|
||||
lyric_line: str = "",
|
||||
) -> MineNote:
|
||||
"""
|
||||
将Midi信息转为我的世界音符对象
|
||||
|
||||
Parameters
|
||||
------------
|
||||
inst_: int
|
||||
乐器编号
|
||||
note_: int
|
||||
音高编号(音符编号)
|
||||
percussive_: bool
|
||||
是否作为打击乐器启用
|
||||
volume_: int
|
||||
音量
|
||||
velocity_: int
|
||||
力度
|
||||
panning_: int
|
||||
声相偏移
|
||||
start_time_: int
|
||||
音符起始时间(微秒)
|
||||
duration_: int
|
||||
音符持续时间(微秒)
|
||||
play_speed: float
|
||||
曲目播放速度
|
||||
midi_reference_table: Dict[int, str]
|
||||
转换对照表
|
||||
volume_processing_method_: Callable[[float], float]
|
||||
音量处理函数
|
||||
panning_processing_method_: Callable[[float], float]
|
||||
立体声相偏移处理函数
|
||||
note_table_replacement: Dict[str, str]
|
||||
音符替换表,定义 Minecraft 音符字串的替换
|
||||
lyric_line: str
|
||||
该音符的歌词
|
||||
|
||||
Returns
|
||||
---------
|
||||
MineNote
|
||||
我的世界音符对象
|
||||
"""
|
||||
mc_sound_ID = midi_inst_to_mc_sound(
|
||||
inst_,
|
||||
midi_reference_table,
|
||||
"note.bd" if percussive_ else "note.flute",
|
||||
)
|
||||
|
||||
return MineNote(
|
||||
mc_sound_name=note_table_replacement.get(mc_sound_ID, mc_sound_ID),
|
||||
midi_pitch=note_,
|
||||
midi_velocity=velocity_,
|
||||
start_time=(tk := int(start_time_ / float(play_speed) / 50000)),
|
||||
last_time=round(duration_ / float(play_speed) / 50000),
|
||||
mass_precision_time=round((start_time_ / float(play_speed) - tk * 50000) / 800),
|
||||
is_percussion=percussive_,
|
||||
distance=volume_processing_method_(volume_),
|
||||
azimuth=(panning_processing_method_(panning_), 0),
|
||||
extra_information={
|
||||
"LYRIC_TEXT": lyric_line,
|
||||
"VOLUME_VALUE": volume_,
|
||||
"PIN_VALUE": panning_,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def midi_msgs_to_minenote_using_kami_respack(
|
||||
inst_: int, # 乐器编号
|
||||
note_: int,
|
||||
@@ -549,11 +426,10 @@ def load_decode_fsq_flush_release(
|
||||
)
|
||||
except Exception as _err:
|
||||
# print(bytes_buffer_in[stt_index:end_index])
|
||||
raise MusicSequenceDecodeError(
|
||||
_err,
|
||||
raise SingleNoteDecodeError(
|
||||
"所截取的音符码之首个字节:",
|
||||
_first_byte,
|
||||
)
|
||||
) from _err
|
||||
|
||||
|
||||
def load_decode_msq_flush_release(
|
||||
@@ -600,8 +476,8 @@ def load_decode_msq_flush_release(
|
||||
|
||||
_total_note_count = 1
|
||||
|
||||
_channel_infos = empty_midi_channels(
|
||||
default_staff={"NOW_INDEX": 0, "NOTE_COUNT": 0, "HAVE_READ": 0, "END_INDEX": -1}
|
||||
_channel_infos = enumerated_stuffcopy_dictionary(
|
||||
staff={"NOW_INDEX": 0, "NOTE_COUNT": 0, "HAVE_READ": 0, "END_INDEX": -1}
|
||||
)
|
||||
|
||||
for __channel_index in _channel_infos.keys():
|
||||
@@ -730,7 +606,7 @@ def load_decode_msq_flush_release(
|
||||
_total_note_count -= 1
|
||||
except Exception as _err:
|
||||
# print(channels_)
|
||||
raise MusicSequenceDecodeError("难以定位的解码错误", _err)
|
||||
raise SingleNoteDecodeError("难以定位的解码错误") from _err
|
||||
if not _read_in_note_list:
|
||||
break
|
||||
# _note_list.append
|
||||
@@ -21,7 +21,7 @@ print(t := msct.midi_2_music_plugin.load(Path("./resources/测试片段.mid"), N
|
||||
t = _global_plugin_registry._music_input_plugins["midi_2_music_plugin"].load(
|
||||
Path("./resources/测试片段.mid"),
|
||||
MidiImportConfig(
|
||||
speed=1.0,
|
||||
speed_multiplier=1.0,
|
||||
),
|
||||
)
|
||||
# 或者
|
||||
|
||||
Reference in New Issue
Block a user