diff --git a/Musicreater/_plugin_abc.py b/Musicreater/_plugin_abc.py index f4029db..bcede3e 100644 --- a/Musicreater/_plugin_abc.py +++ b/Musicreater/_plugin_abc.py @@ -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": """从字典创建配置实例 参数 diff --git a/Musicreater/_utils.py b/Musicreater/_utils.py new file mode 100644 index 0000000..2acb3ae --- /dev/null +++ b/Musicreater/_utils.py @@ -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)} diff --git a/Musicreater/builtin_plugins/midi_read/constants.py b/Musicreater/builtin_plugins/midi_read/constants.py index 31f59d1..e633003 100644 --- a/Musicreater/builtin_plugins/midi_read/constants.py +++ b/Musicreater/builtin_plugins/midi_read/constants.py @@ -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", diff --git a/Musicreater/builtin_plugins/midi_read/exceptions.py b/Musicreater/builtin_plugins/midi_read/exceptions.py new file mode 100644 index 0000000..653f9fb --- /dev/null +++ b/Musicreater/builtin_plugins/midi_read/exceptions.py @@ -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) diff --git a/Musicreater/builtin_plugins/midi_read/main.py b/Musicreater/builtin_plugins/midi_read/main.py index 1a7ac85..cb7558e 100644 --- a/Musicreater/builtin_plugins/midi_read/main.py +++ b/Musicreater/builtin_plugins/midi_read/main.py @@ -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 diff --git a/Musicreater/builtin_plugins/midi_read/utils.py b/Musicreater/builtin_plugins/midi_read/utils.py index f2f9557..1a37fcd 100644 --- a/Musicreater/builtin_plugins/midi_read/utils.py +++ b/Musicreater/builtin_plugins/midi_read/utils.py @@ -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), + ) diff --git a/Musicreater/constants.py b/Musicreater/constants.py index 63eae0d..59b6ebe 100644 --- a/Musicreater/constants.py +++ b/Musicreater/constants.py @@ -34,15 +34,6 @@ z = "z" z """ -MIDI_PROGRAM = "program" -"""Midi乐器编号""" - -MIDI_VOLUME = "volume" -"""Midi通道音量""" - -MIDI_PAN = "pan" -"""Midi通道立体声场偏移""" - # Midi用对照表 diff --git a/Musicreater/data.py b/Musicreater/data.py index 8b6be8c..e1f9aae 100644 --- a/Musicreater/data.py +++ b/Musicreater/data.py @@ -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 diff --git a/Musicreater/exceptions.py b/Musicreater/exceptions.py index bca8663..4eaa347 100644 --- a/Musicreater/exceptions.py +++ b/Musicreater/exceptions.py @@ -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) diff --git a/Musicreater/main.py b/Musicreater/main.py index 1ec2b6a..085a499 100644 --- a/Musicreater/main.py +++ b/Musicreater/main.py @@ -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: diff --git a/Musicreater/paramcurve.py b/Musicreater/paramcurve.py index e7f70c5..d8645b7 100644 --- a/Musicreater/paramcurve.py +++ b/Musicreater/paramcurve.py @@ -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 diff --git a/Musicreater/plugins.py b/Musicreater/plugins.py index 71874e2..c4fe912 100644 --- a/Musicreater/plugins.py +++ b/Musicreater/plugins.py @@ -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 ( diff --git a/Musicreater/types.py b/Musicreater/types.py index 9010b95..3d43c10 100644 --- a/Musicreater/types.py +++ b/Musicreater/types.py @@ -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] -""" -拟合函数类型 -""" diff --git a/TO-DO.md b/TO-DO.md index e400a16..a298356 100644 --- a/TO-DO.md +++ b/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. [] 插件依赖性的优化。目前没有处理各个插件依赖关系的问题,如果插件之间彼此依赖要怎么做? 我的想法是,这个依赖的处理由调用端来完成。比如我们的 伶伦工作站 是以 音·创 为核心的一个可视化数字音频工作站。 那么应该由伶伦来处理依赖关系并加载之。 diff --git a/docs/异常继承关系.mmd b/docs/异常继承关系.mmd new file mode 100644 index 0000000..8ef8523 --- /dev/null +++ b/docs/异常继承关系.mmd @@ -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 \ No newline at end of file diff --git a/docs/异常继承关系.svg b/docs/异常继承关系.svg new file mode 100644 index 0000000..3f84808 --- /dev/null +++ b/docs/异常继承关系.svg @@ -0,0 +1,67 @@ +

Exception

Python 内置基类

MusicreaterBaseException

"[音·创] - ..."

所有音·创 v3 错误的基类

MusicreaterInnerlyError

"内部错误 - ..."

面向开发者的内部错误

MusicreaterOuterlyError

"外部错误 - ..."

面向用户的外部错误

InnerlyParameterError

"内部传参错误 - ..."

内部参数相关错误

OuterlyParameterError

"参数错误 - ..."

外部参数相关错误

ParameterTypeError

"参数类型错误:..."

继承自 InnerlyParameterError 和 TypeError

ParameterValueError

"参数数值错误:..."

继承自 InnerlyParameterError 和 ValueError

PluginNotSpecifiedError

"未指定插件:..."

继承自 InnerlyParameterError 和 LookupError

ZeroSpeedError

"播放速度为零:..."

继承自 OuterlyParameterError 和 ZeroDivisionError

IllegalMinimumVolumeError

"最小播放音量超出范围:..."

继承自 OuterlyParameterError 和 ValueError

FileFormatNotSupportedError

"不支持的文件格式:..."

继承自 MusicreaterOuterlyError

NoteBinaryDecodeError

"解码音乐存储二进制数据时出现问题 - ..."

继承自 MusicreaterOuterlyError

SingleNoteDecodeError

"音符解码出错:..."

继承自 NoteBinaryDecodeError

NoteBinaryFileTypeError

"无法识别音乐存储文件对应的类型:..."

继承自 NoteBinaryDecodeError

NoteBinaryFileVerificationFailed

"音乐存储文件校验失败:..."

继承自 NoteBinaryDecodeError

PluginDefineError

"插件内部错误 - ..."

插件定义相关的内部错误

PluginInstanceNotFoundError

"插件实例未找到:..."

继承自 PluginDefineError 和 LookupError

PluginAttributeNotFoundError

"插件类的必要属性不存在:..."

继承自 PluginDefineError 和 AttributeError

PluginMetainfoError

"插件元信息定义错误 - ..."

插件元信息相关错误

PluginMetainfoTypeError

"插件元信息类型错误:..."

继承自 PluginMetainfoError 和 TypeError

PluginMetainfoValueError

"插件元信息数值错误:..."

继承自 PluginMetainfoError 和 ValueError

PluginMetainfoNotFoundError

"插件元信息未定义:..."

继承自 PluginMetainfoError 和 PluginAttributeNotFoundError

PluginLoadError

"插件加载错误 - ..."

插件加载相关的外部错误

PluginNotFoundError

"插件未找到:..."

继承自 PluginLoadError

PluginRegisteredError

"插件重复注册:..."

继承自 PluginLoadError

PluginConfigRelatedError

"插件配置相关错误 - ..."

插件配置相关错误基类

PluginConfigLoadError

"插件配置文件加载错误:..."

继承自 PluginLoadError、PluginConfigRelatedError

PluginConfigDumpError

"插件配置文件保存错误:..."

继承自 PluginConfigRelatedError

TypeError

ValueError

LookupError

AttributeError

ZeroDivisionError

\ No newline at end of file diff --git a/examples/doc_importdata_plugin.md b/examples/doc_importdata_plugin.md new file mode 100644 index 0000000..588b39e --- /dev/null +++ b/examples/doc_importdata_plugin.md @@ -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": + ... +``` + +至此,一个插件的编写已经完成。 \ No newline at end of file diff --git a/examples/exp_importdata_plugin.py b/examples/exp_importdata_plugin.py new file mode 100644 index 0000000..6858acb --- /dev/null +++ b/examples/exp_importdata_plugin.py @@ -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() diff --git a/old-things/Musicreater/experiment.py b/old-things/Musicreater/experiment.py index 64f1a76..2b0e40b 100644 --- a/old-things/Musicreater/experiment.py +++ b/old-things/Musicreater/experiment.py @@ -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: diff --git a/old-things/Musicreater/old_exceptions.py b/old-things/Musicreater/old_exceptions.py index 787e3d8..5ebd52e 100644 --- a/old-things/Musicreater/old_exceptions.py +++ b/old-things/Musicreater/old_exceptions.py @@ -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) diff --git a/old-things/Musicreater/old_main.py b/old-things/Musicreater/old_main.py index 0491553..51e985b 100644 --- a/old-things/Musicreater/old_main.py +++ b/old-things/Musicreater/old_main.py @@ -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] = {}, diff --git a/old-things/Musicreater/utils.py b/old-things/Musicreater/old_utils.py similarity index 87% rename from old-things/Musicreater/utils.py rename to old-things/Musicreater/old_utils.py index f59d434..7bc97b9 100644 --- a/old-things/Musicreater/utils.py +++ b/old-things/Musicreater/old_utils.py @@ -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 diff --git a/test_read.py b/test_read.py index 493866a..2f9b0c3 100644 --- a/test_read.py +++ b/test_read.py @@ -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, ), ) # 或者