From c14489f3a759fac2d7fd7448343e7c25d6a8c841 Mon Sep 17 00:00:00 2001 From: EillesWan Date: Tue, 8 Apr 2025 17:49:49 +0800 Subject: [PATCH] =?UTF-8?q?MSQ=20=E6=B5=81=E5=BC=8F=E9=80=82=E9=85=8D?= =?UTF-8?q?=E4=B8=8E=E6=A0=A1=E9=AA=8C=E5=A2=9E=E5=BC=BA=EF=BC=8C=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=20NBS=20=E9=9F=B3=E8=89=B2=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Musicreater/__init__.py | 12 +- Musicreater/constants.py | 216 +++++++++++++++++++++++++- Musicreater/main.py | 17 ++- Musicreater/subclass.py | 19 +-- Musicreater/types.py | 17 ++- Musicreater/utils.py | 320 +++++++++++++++++++++++++++++++++++++-- README.md | 25 ++- README_EN.md | 20 +-- clean_update.py | 4 +- docs/MSQ文件格式.md | 12 +- example.py | 2 +- example_msq_opera.py | 27 +++- let_future_java.py | 13 +- 13 files changed, 629 insertions(+), 75 deletions(-) diff --git a/Musicreater/__init__.py b/Musicreater/__init__.py index 60a2273..06d760c 100644 --- a/Musicreater/__init__.py +++ b/Musicreater/__init__.py @@ -22,8 +22,8 @@ The Licensor of Musicreater("this project") is Eilles Wan, bgArray. # 若需转载或借鉴 许可声明请查看仓库目录下的 License.md -__version__ = "2.2.3" -__vername__ = "Java版欲适配、初步MSQ流式适配" +__version__ = "2.2.4" +__vername__ = "MSQ 流式适配与校验增强,新增 NBS 音色表" __author__ = ( ("金羿", "Eilles"), ("诸葛亮与八卦阵", "bgArray"), @@ -48,6 +48,14 @@ __all__ = [ "MM_CLASSIC_PERCUSSION_INSTRUMENT_TABLE", "MM_TOUCH_PITCHED_INSTRUMENT_TABLE", "MM_TOUCH_PERCUSSION_INSTRUMENT_TABLE", + "MM_NBS_PITCHED_INSTRUMENT_TABLE", + "MM_NBS_PERCUSSION_INSTRUMENT_TABLE", + # 操作性函数 + "natural_curve", + "straight_line", + "load_decode_msq_metainfo", + "load_decode_msq_flush_release", + "guess_deviation", ] from .main import * diff --git a/Musicreater/constants.py b/Musicreater/constants.py index e331335..ea971de 100644 --- a/Musicreater/constants.py +++ b/Musicreater/constants.py @@ -16,7 +16,8 @@ Terms & Conditions: License.md in the root directory # Email TriM-Organization@hotmail.com # 若需转载或借鉴 许可声明请查看仓库目录下的 License.md -from .types import Dict, List, Tuple, MidiInstrumentTableType, MidiNoteNameTableType +# from .types import Dict, List, Tuple, MidiInstrumentTableType, MidiNoteNameTableType +from typing import Dict, List, Tuple x = "x" """ @@ -478,7 +479,12 @@ MM_INSTRUMENT_DEVIATION_TABLE: Dict[str, int] = { "fire.ignite": 0, "note.cow_bell": 6, } -"""不同乐器的音调偏离对照表""" +""" +不同乐器的音调偏离对照表 +*注意* 该表中的单位是对于 Midi Pitch 音调(整数)的低音偏移。 +也就是说,该数值越高,则在 Midi Pitch 中的值域越低 +默认的偏移量为 6 ,因为在计算音高时候少减去了 6 个 Pitch 单位 +""" # Midi乐器对MC乐器对照表 @@ -853,6 +859,8 @@ MM_TOUCH_PERCUSSION_INSTRUMENT_TABLE: Dict[int, str] = { """“偷吃”打击乐器对照表""" # Dislink “断联” 音色对照表 +# https://github.com/Dislink/midi2bdx/blob/main/index.html + MM_DISLINK_PITCHED_INSTRUMENT_TABLE: Dict[int, str] = { 0: "note.harp", @@ -1037,6 +1045,210 @@ MM_DISLINK_PERCUSSION_INSTRUMENT_TABLE: Dict[int, str] = { } """“断联”打击乐器对照表""" +# NoteBlockStudio “NBS”音色对照表 +# https://github.com/OpenNBS/NoteBlockStudio/blob/main/scripts/midi_instruments/midi_instruments.gml + +MM_NBS_PITCHED_INSTRUMENT_TABLE: Dict[int, str] = { + 0: "note.harp", + 1: "note.pling", + 2: "note.harp", + 3: "note.pling", + 4: "note.harp", + 5: "note.harp", + 6: "note.guitar", + 7: "note.banjo", + 8: "note.bell", + 9: "note.bell", + 10: "note.bell", + 11: "note.iron_xylophone", + 12: "note.iron_xylophone", + 13: "note.xylophone", + 14: "note.bell", + 15: "note.iron_xylophone", + 16: "note.flute", + 17: "note.flute", + 18: "note.flute", + 19: "note.flute", + 20: "note.flute", + 21: "note.flute", + 22: "note.flute", + 23: "note.flute", + 24: "note.guitar", + 25: "note.guitar", + 26: "note.guitar", + 27: "note.bass", + 28: "note.guitar", + 29: "note.guitar", + 30: "note.bass", + 31: "note.bass", + 32: "note.bass", + 33: "note.guitar", + 34: "note.guitar", + 35: "note.bass", + 36: "note.pling", + 37: "note.flute", + 38: "note.flute", + 39: "note.flute", + 40: "note.flute", + 41: "note.flute", + 42: "note.didgeridoo", + 43: "note.flute", + 44: "note.didgeridoo", + 45: "note.flute", + 46: "note.flute", + 47: "note.flute", + 48: "note.flute", + 49: "note.flute", + 50: "note.flute", + 51: "note.flute", + 52: "note.flute", + 53: "note.flute", + 54: "note.flute", + 55: "note.flute", + 56: "note.flute", + 57: "note.flute", + 58: "note.flute", + 59: "note.flute", + 60: "note.bit", + 61: "note.flute", + 62: "note.flute", + 63: "note.flute", + 64: "note.flute", + 65: "note.guitar", + 66: "note.flute", + 67: "note.flute", + 68: "note.flute", + 69: "note.bell", + 70: "note.flute", + 71: "note.flute", + 72: "note.flute", + 73: "note.flute", + 74: "note.chime", + 75: "note.flute", + 76: "note.flute", + 77: "note.guitar", + 78: "note.pling", + 79: "note.flute", + 80: "note.guitar", + 81: "note.banjo", + 82: "note.banjo", + 83: "note.banjo", + 84: "note.guitar", + 85: "note.iron_xylophone", + 86: "note.flute", + 87: "note.flute", + 88: "note.chime", + 89: "note.cow_bell", + 90: "note.iron_xylophone", + 91: "note.xylophone", + 92: "note.basedrum", + 93: "note.snare", + 94: "note.snare", + 95: "note.basedrum", + 96: "note.snare", + 97: "note.hat", + 98: "note.snare", + 99: "note.hat", + 100: "note.basedrum", + 101: "note.hat", + 102: "note.basedrum", + 103: "note.hat", + 104: "note.basedrum", + 105: "note.snare", + 106: "note.snare", + 107: "note.snare", + 108: "note.cow_bell", + 109: "note.snare", + 110: "note.hat", + 111: "note.snare", + 112: "note.hat", + 113: "note.hat", + 114: "note.hat", + 115: "note.hat", + 116: "note.hat", + 117: "note.chime", + 118: "note.hat", + 119: "note.snare", + 120: "note.hat", + 121: "note.hat", + 122: "note.hat", + 123: "note.hat", + 124: "note.hat", + 125: "note.snare", + 126: "note.basedrum", + 127: "note.basedrum", +} +"""“NBS”乐音乐器对照表""" + + +MM_NBS_PERCUSSION_INSTRUMENT_TABLE: Dict[int, str] = { + 24: "note.bit", + 25: "note.snare", + 26: "note.hat", + 27: "note.snare", + 28: "note.snare", + 29: "note.hat", + 30: "note.hat", + 31: "note.hat", + 32: "note.hat", + 33: "note.hat", + 34: "note.chime", + 35: "note.basedrum", + 36: "note.basedrum", + 37: "note.hat", + 38: "note.snare", + 39: "note.hat", + 40: "note.snare", + 41: "note.basedrum", + 42: "note.snare", + 43: "note.basedrum", + 44: "note.snare", + 45: "note.basedrum", + 46: "note.basedrum", + 47: "note.snare", + 48: "note.snare", + 49: "note.snare", + 50: "note.snare", + 51: "note.snare", + 52: "note.snare", + 53: "note.hat", + 54: "note.snare", + 55: "note.snare", + 56: "note.cow_bell", + 57: "note.snare", + 58: "note.hat", + 59: "note.snare", + 60: "note.hat", + 61: "note.hat", + 62: "note.hat", + 63: "note.basedrum", + 64: "note.basedrum", + 65: "note.snare", + 66: "note.snare", + 67: "note.xylophone", + 68: "note.xylophone", + 69: "note.hat", + 70: "note.hat", + 71: "note.flute", + 72: "note.flute", + 73: "note.hat", + 74: "note.hat", + 75: "note.hat", + 76: "note.hat", + 77: "note.hat", + 78: "note.didgeridoo", + 79: "note.didgeridoo", + 80: "note.hat", + 81: "note.chime", + 82: "note.hat", + 83: "note.chime", + 84: "note.chime", + 85: "note.hat", + 86: "note.basedrum", + 87: "note.basedrum", +} +"""“NBS”打击乐器对照表""" + # Midi音高对MC方块对照表 # 金羿ELS 音符方块对照表 diff --git a/Musicreater/main.py b/Musicreater/main.py index a13ec96..2fdffda 100644 --- a/Musicreater/main.py +++ b/Musicreater/main.py @@ -34,7 +34,6 @@ import os import math import mido -from xxhash import xxh3_64, xxh3_128 from .constants import * from .exceptions import * @@ -169,6 +168,7 @@ class MusicSequence: minimum_vol: float = 0.1, volume_processing_function: FittingFunctionType = natural_curve, deviation: float = 0, + note_referance_table_replacement: Dict[str, str] = {}, ): """ 自mido对象导入一个音符序列类 @@ -210,6 +210,7 @@ class MusicSequence: default_tempo_value=default_tempo, vol_processing_function=volume_processing_function, ignore_mismatch_error=mismatch_error_ignorance, + note_rtable_replacement=note_referance_table_replacement, ) else: note_channels = {} @@ -241,6 +242,7 @@ class MusicSequence: music_name_ = bytes_buffer_in[8 : (stt_index := 8 + (group_1 >> 10))].decode( "GB18030" ) + channels_: MineNoteChannelType = empty_midi_channels(default_staff=[]) total_note_count = 0 if verify: @@ -477,8 +479,11 @@ class MusicSequence: """重命名此音乐""" self.music_name = new_name - def add_note(self, channel_no: int, note: MineNote, is_sort: bool = False): - """在指定通道添加一个音符""" + def add_note(self, channel_no: int, note: MineNote, is_sort: bool = True): + """ + 在指定通道添加一个音符 + 值得注意:在版本 2.2.3 及之前 is_sort 参数默认为 False ;在此之后为 True + """ self.channels[channel_no].append(note) self.total_note_count += 1 if note.sound_name in self.note_count_per_instrument.keys(): @@ -498,6 +503,7 @@ class MusicSequence: pitched_note_rtable: MidiInstrumentTableType = MM_TOUCH_PITCHED_INSTRUMENT_TABLE, percussion_note_rtable: MidiInstrumentTableType = MM_TOUCH_PERCUSSION_INSTRUMENT_TABLE, vol_processing_function: FittingFunctionType = natural_curve, + note_rtable_replacement: Dict[str, str] = {}, ) -> Tuple[MineNoteChannelType, int, Dict[str, int]]: """ 将midi解析并转换为频道音符字典 @@ -616,6 +622,7 @@ class MusicSequence: else pitched_note_rtable ), volume_processing_method_=vol_processing_function, + note_table_replacement=note_rtable_replacement, ) ) note_count += 1 @@ -693,6 +700,7 @@ class MidiConvert(MusicSequence): enable_old_exe_format: bool = False, minimum_volume: float = 0.1, vol_processing_function: FittingFunctionType = natural_curve, + note_rtable_replacement: Dict[str, str] = {}, ): """ 简单的midi转换类,将midi对象转换为我的世界结构或者包 @@ -742,6 +750,7 @@ class MidiConvert(MusicSequence): volume_processing_function=vol_processing_function, default_tempo=default_tempo_value, mismatch_error_ignorance=ignore_mismatch_error, + note_referance_table_replacement=note_rtable_replacement, ) @classmethod @@ -756,6 +765,7 @@ class MidiConvert(MusicSequence): old_exe_format: bool = False, min_volume: float = 0.1, vol_processing_func: FittingFunctionType = natural_curve, + note_table_replacement: Dict[str, str] = {}, ): """ 直接输入文件地址,将midi文件读入 @@ -802,6 +812,7 @@ class MidiConvert(MusicSequence): enable_old_exe_format=old_exe_format, minimum_volume=min_volume, vol_processing_function=vol_processing_func, + note_rtable_replacement=note_table_replacement, ) except (ValueError, TypeError) as E: raise MidiDestroyedError(f"文件{midi_file_path}可能损坏:{E}") diff --git a/Musicreater/subclass.py b/Musicreater/subclass.py index 66636ca..bb126e4 100644 --- a/Musicreater/subclass.py +++ b/Musicreater/subclass.py @@ -18,9 +18,9 @@ Terms & Conditions: License.md in the root directory from dataclasses import dataclass -from .types import Optional, Any, List, Mapping, Tuple, Union +from typing import Optional, Any, List, Tuple, Union -from .constants import MC_PERCUSSION_INSTRUMENT_LIST +from .constants import MC_PITCHED_INSTRUMENT_LIST @dataclass(init=False) @@ -91,7 +91,7 @@ class MineNote: """高精度开始时间偏量 0.4 毫秒""" self.percussive = ( - (mc_sound_name in MC_PERCUSSION_INSTRUMENT_LIST) + (mc_sound_name not in MC_PITCHED_INSTRUMENT_LIST) if (is_percussion is None) else is_percussion ) @@ -463,7 +463,7 @@ class SingleNoteBox: self.annotation_text = annotation """音符注释""" if percussion is None: - self.is_percussion = percussion in MC_PERCUSSION_INSTRUMENT_LIST + self.is_percussion = percussion not in MC_PITCHED_INSTRUMENT_LIST else: self.is_percussion = percussion @@ -653,14 +653,3 @@ DEFAULT_PROGRESSBAR_STYLE = ProgressBarStyle( """ 默认的进度条样式 """ - - -MineNoteChannelType = Mapping[ - int, - List[MineNote,], -] -""" -我的世界频道信息类型 - -Dict[int,Dict[int,List[MineNote,],],] -""" diff --git a/Musicreater/types.py b/Musicreater/types.py index 6f18b88..a585cf2 100644 --- a/Musicreater/types.py +++ b/Musicreater/types.py @@ -17,19 +17,17 @@ Terms & Conditions: License.md in the root directory # 若需转载或借鉴 许可声明请查看仓库目录下的 License.md from typing import ( - Any, Dict, List, Literal, - Optional, Tuple, Union, - Iterable, - Sequence, Mapping, Callable, ) +from .subclass import MineNote + MidiNoteNameTableType = Mapping[int, Tuple[str, ...]] """ @@ -64,3 +62,14 @@ ChannelType = Dict[ Dict[int,Dict[int,List[Union[Tuple[Literal["PgmC"], int, int],Tuple[Literal["NoteS"], int, int, int],Tuple[Literal["NoteE"], int, int],]],],] """ + + +MineNoteChannelType = Mapping[ + int, + List[MineNote,], +] +""" +我的世界频道信息类型 + +Dict[int,Dict[int,List[MineNote,],],] +""" diff --git a/Musicreater/utils.py b/Musicreater/utils.py index d2eb470..466d7a8 100644 --- a/Musicreater/utils.py +++ b/Musicreater/utils.py @@ -18,6 +18,22 @@ Terms & Conditions: License.md in the root directory import math import random +# from io import BytesIO +from typing import ( + Any, + Dict, + Tuple, + Optional, + Callable, + Literal, + Union, + List, + Generator, + BinaryIO, +) + +from xxhash import xxh3_64, xxh3_128 + from .constants import ( MC_INSTRUMENT_BLOCKS_TABLE, MM_INSTRUMENT_DEVIATION_TABLE, @@ -26,16 +42,9 @@ from .constants import ( ) from .subclass import MineNote, mctick2timestr -from .types import ( - Any, - Dict, - Tuple, - Optional, - Callable, - Literal, - Union, - MidiInstrumentTableType, -) +from .types import MidiInstrumentTableType, MineNoteChannelType + +from .exceptions import MusicSequenceDecodeError def empty_midi_channels( @@ -251,6 +260,7 @@ def midi_msgs_to_minenote( play_speed: float, midi_reference_table: MidiInstrumentTableType, volume_processing_method_: Callable[[float], float], + note_table_replacement: Dict[str, str] = {}, ) -> MineNote: """ 将Midi信息转为我的世界音符对象 @@ -275,7 +285,7 @@ def midi_msgs_to_minenote( mc_distance_volume = volume_processing_method_(velocity_) return MineNote( - mc_sound_name=mc_sound_ID, + 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)), @@ -378,3 +388,291 @@ def soundID_to_blockID( return random.choice(MC_INSTRUMENT_BLOCKS_TABLE.get(sound_id, (default_block,))) else: return MC_INSTRUMENT_BLOCKS_TABLE.get(sound_id, (default_block,))[0] + + +def load_decode_msq_metainfo( + buffer_in: BinaryIO, +) -> Tuple[str, float, float, bool, int]: + """ + 以流的方式解码MSQ音乐序列元信息 + + Parameters + ---------- + buffer_in: BytesIO + MSQ格式的字节流 + + Returns + ------- + Tuple[str, float, float, bool, int] + 音乐名称,最小音量,音调偏移,是否启用高精度,最后的流指针位置 + + """ + buffer_in.seek(4, 0) + group_1 = int.from_bytes(buffer_in.read(2), "big") + group_2 = int.from_bytes(buffer_in.read(2), "big", signed=False) + + # high_quantity = bool(group_2 & 0b1000000000000000) + # print(group_2, high_quantity) + + music_name_ = buffer_in.read(stt_index := (group_1 >> 10)).decode("GB18030") + + return ( + music_name_, + (group_1 & 0b1111111111) / 1000, + ( + (-1 if group_2 & 0b100000000000000 else 1) + * (group_2 & 0b11111111111111) + / 1000 + ), + bool(group_2 & 0b1000000000000000), + stt_index + 8, + ) + + +def load_decode_msq_flush_release( + buffer_in: BinaryIO, + starter_index: int, + high_quantity_note: bool, +) -> Generator[Tuple[int, MineNote], Any, None]: + """以流的方式解码MSQ音乐序列的音符序列并流式返回 + + Parameters + ---------- + buffer_in : BytesIO + 输入的MSQ格式二进制字节流 + starter_index : int + 字节流中,音符序列的起始索引 + high_quantity_note : bool + 是否启用高精度音符解析 + + Returns + ------- + Generator[Tuple[int, MineNote], Any, None] + 以流的方式返回解码后的音符序列 + + Raises + ------ + MusicSequenceDecodeError + 当解码过程中出现错误,抛出异常 + + """ + + # _total_verify = xxh3_64(buffer_in.read(starter_index), seed=total_note_count) + + # buffer_in.seek(starter_index, 0) + buffer_in.seek(starter_index) + _bytes_buffer_in = buffer_in.read() + # int.from_bytes(_bytes_buffer_in[0 : 4], "big") + + _now_channel_starter_index = 0 + + _total_note_count = 1 + + _channel_infos = empty_midi_channels( + default_staff={"NOW_INDEX": 0, "NOTE_COUNT": 0, "HAVE_READ": 0, "END_INDEX": -1} + ) + + for __channel_index in _channel_infos.keys(): + # _channel_note_count = 0 + + _now_channel_ender_sign = xxh3_64( + _bytes_buffer_in[ + _now_channel_starter_index : _now_channel_starter_index + 4 + ], + seed=3, + ).digest() + + # print( + # "[DEBUG] 索引取得:", + # _bytes_buffer_in[ + # _now_channel_starter_index : _now_channel_starter_index + 4 + # ], + # "校验索引", + # _now_channel_ender_sign, + # ) + + _now_channel_ender_index = _bytes_buffer_in.find(_now_channel_ender_sign) + + # print("[DEBUG] 索引取得:", _now_channel_ender_index,) + + _channel_note_count = int.from_bytes( + _bytes_buffer_in[ + _now_channel_starter_index : _now_channel_starter_index + 4 + ], + "big", + ) + + if _channel_note_count == 0: + continue + + while ( + xxh3_64( + _bytes_buffer_in[_now_channel_starter_index:_now_channel_ender_index], + seed=_channel_note_count, + ).digest() + != _bytes_buffer_in[ + _now_channel_ender_index + 8 : _now_channel_ender_index + 16 + ] + ): + _now_channel_ender_index += 8 + _bytes_buffer_in[ + _now_channel_ender_index + 8 : + ].find(_now_channel_ender_sign) + + # print( + # "[WARNING] XXHASH 无法匹配,当前序列", + # __channel_index, + # "当前全部序列字节串", + # _bytes_buffer_in[ + # _now_channel_starter_index:_now_channel_ender_index + # ], + # "校验值", + # xxh3_64( + # _bytes_buffer_in[ + # _now_channel_starter_index:_now_channel_ender_index + # ], + # seed=_channel_note_count, + # ).digest(), + # _bytes_buffer_in[ + # _now_channel_ender_index + 8 : _now_channel_ender_index + 16 + # ], + # "改变结尾索引", + # _now_channel_ender_index, + # ) + + _channel_infos[__channel_index]["NOW_INDEX"] = _now_channel_starter_index + 4 + _channel_infos[__channel_index]["END_INDEX"] = _now_channel_ender_index + _channel_infos[__channel_index]["NOTE_COUNT"] = _channel_note_count + + # print( + # "[DEBUG] 当前序列", __channel_index, "值", _channel_infos[__channel_index] + # ) + + _total_note_count += _channel_note_count + + _now_channel_starter_index = _now_channel_ender_index + 16 + # for i in range( + # int.from_bytes( + # bytes_buffer_in[stt_index : (stt_index := stt_index + 4)], "big" + # ) + # ): + _to_yield_note_list: List[Tuple[MineNote, int]] = [] + + # {"NOW_INDEX": 0, "NOTE_COUNT": 0, "HAVE_READ": 0, "END_INDEX": -1} + + while _total_note_count: + _read_in_note_list: List[Tuple[MineNote, int]] = [] + for __channel_index in _channel_infos.keys(): + if ( + _channel_infos[__channel_index]["HAVE_READ"] + < _channel_infos[__channel_index]["NOTE_COUNT"] + ): + # print("当前已读", _channel_infos[__channel_index]["HAVE_READ"]) + try: + _end_index = ( + (_stt_index := _channel_infos[__channel_index]["NOW_INDEX"]) + + 13 + + high_quantity_note + + (_bytes_buffer_in[_stt_index] >> 2) + ) + # print("读取音符字节串", _bytes_buffer_in[_stt_index:_end_index]) + _read_in_note_list.append( + ( + MineNote.decode( + code_buffer=_bytes_buffer_in[_stt_index:_end_index], + is_high_time_precision=high_quantity_note, + ), + __channel_index, + ) + ) + _channel_infos[__channel_index]["HAVE_READ"] += 1 + _channel_infos[__channel_index]["NOW_INDEX"] = _end_index + _total_note_count -= 1 + except Exception as _err: + # print(channels_) + raise MusicSequenceDecodeError("难以定位的解码错误", _err) + if not _read_in_note_list: + break + # _note_list.append + min_stt_note: MineNote = min(_read_in_note_list, key=lambda x: x[0].start_tick)[ + 0 + ] + for i in range(len(_to_yield_note_list)): + __note, __channel_index = _to_yield_note_list[i] + if __note.start_tick >= min_stt_note.start_tick: + break + else: + yield __channel_index, __note + _to_yield_note_list.pop(i) + + _to_yield_note_list.extend(_read_in_note_list) + _to_yield_note_list.sort(key=lambda x: x[0].start_tick) + + for __note, __channel_index in sorted( + _to_yield_note_list, key=lambda x: x[0].start_tick + ): + yield __channel_index, __note + # 俺寻思能用 + + +def guess_deviation( + total_note_count: int, + total_instrument_count: int, + note_count_per_instrument: Optional[Dict[str, int]] = None, + qualified_note_count_per_instrument: Optional[Dict[str, int]] = None, + music_channels: Optional[MineNoteChannelType] = None, +) -> float: + """ + 通过乐器权重来计算一首歌的音调偏移 + 这个方法未经验证,但理论有效,金羿首创 + + Parameters + ---------- + total_note_count: int + 歌曲总音符数 + total_instrument_count: int + 歌曲乐器总数 + note_count_per_instrument: Dict[str, int] + 乐器名称与乐器音符数对照表 + qualified_note_count_per_instrument: Dict[str, int] + 每个乐器中,符合该乐器的音调范围的音符数 + music_channels: MineNoteChannelType + MusicSequence类的音乐通道字典 + + Returns + ------- + float估测的音调偏移值 + """ + if note_count_per_instrument is None or qualified_note_count_per_instrument is None: + if music_channels is None: + raise ValueError("参数不足,算逑!") + note_count_per_instrument = {} + qualified_note_count_per_instrument = {} + for this_note in [k for j in music_channels.values() for k in j]: + if this_note.sound_name in note_count_per_instrument.keys(): + note_count_per_instrument[this_note.sound_name] += 1 + qualified_note_count_per_instrument[ + this_note.sound_name + ] += is_note_in_diapason(this_note) + else: + note_count_per_instrument[this_note.sound_name] = 1 + qualified_note_count_per_instrument[this_note.sound_name] = int( + is_note_in_diapason(this_note) + ) + return ( + sum( + [ + ( + ( + MM_INSTRUMENT_RANGE_TABLE[inst][-1] + * note_count + / total_note_count + - MM_INSTRUMENT_RANGE_TABLE[inst][-1] + ) + * (note_count - qualified_note_count_per_instrument[inst]) + ) + for inst, note_count in note_count_per_instrument.items() + ] + ) + / total_instrument_count + / total_note_count + ) diff --git a/README.md b/README.md index 8d9eb69..e7bbe40 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,12 @@ -

- 音·创 Musicreater -

+[Bilibili: 金羿ELS]: https://img.shields.io/badge/Bilibili-%E9%87%91%E7%BE%BFELS-00A1E7?style=for-the-badge +[Bilibili: 诸葛亮与八卦阵]: https://img.shields.io/badge/Bilibili-%E8%AF%B8%E8%91%9B%E4%BA%AE%E4%B8%8E%E5%85%AB%E5%8D%A6%E9%98%B5-00A1E7?style=for-the-badge +[CodeStyle: black]: https://img.shields.io/badge/code%20style-black-121110.svg?style=for-the-badge +[python]: https://img.shields.io/badge/python-3.8-AB70FF?style=for-the-badge +[release]: https://img.shields.io/github/v/release/EillesWan/Musicreater?style=for-the-badge +[license]: https://img.shields.io/badge/Licence-Apache-228B22?style=for-the-badge + + +

音·创 Musicreater

@@ -92,10 +98,10 @@ -
感谢 **油炸**<QQ2836146704> 激励我们不断开发新的内容。
- 感谢 **雨**\ 反馈在新版本的指令格式下,计分板播放器的附加包无法播放的问题。 - 感谢 **梦幻duang**\ 为我们提供 Java 1.12.2 版本命令格式参考。 +- 感谢 [_Open Note Block Studio_](https://github.com/OpenNBS/NoteBlockStudio) 项目的开发为我们提供持续的追赶动力。 -> 感谢广大群友为此库提供的测试和建议等 -> -> 若您对我们有所贡献但您的名字没有出现在此列表中,请联系我们! +> 感谢广大群友为此库提供的测试和建议等 +> 若您对我们有所贡献但您的名字没有出现在此列表中,请联系我们! ## 联系 📞 @@ -122,10 +128,3 @@ NOT AN OFFICIAL MINECRAFT PRODUCT. NOT APPROVED BY OR ASSOCIATED WITH MOJANG OR MICROSOFT. NOT APPROVED BY OR ASSOCIATED WITH NETEASE. - -[Bilibili: 金羿ELS]: https://img.shields.io/badge/Bilibili-%E9%87%91%E7%BE%BFELS-00A1E7?style=for-the-badge -[Bilibili: 诸葛亮与八卦阵]: https://img.shields.io/badge/Bilibili-%E8%AF%B8%E8%91%9B%E4%BA%AE%E4%B8%8E%E5%85%AB%E5%8D%A6%E9%98%B5-00A1E7?style=for-the-badge -[CodeStyle: black]: https://img.shields.io/badge/code%20style-black-121110.svg?style=for-the-badge -[python]: https://img.shields.io/badge/python-3.8-AB70FF?style=for-the-badge -[release]: https://img.shields.io/github/v/release/EillesWan/Musicreater?style=for-the-badge -[license]: https://img.shields.io/badge/Licence-Apache-228B22?style=for-the-badge diff --git a/README_EN.md b/README_EN.md index fc26128..2e1aa2c 100644 --- a/README_EN.md +++ b/README_EN.md @@ -1,3 +1,10 @@ +[Bilibili: Eilles]: https://img.shields.io/badge/Bilibili-%E9%87%91%E7%BE%BFELS-00A1E7?style=for-the-badge +[Bilibili: bgArray]: https://img.shields.io/badge/Bilibili-%E8%AF%B8%E8%91%9B%E4%BA%AE%E4%B8%8E%E5%85%AB%E5%8D%A6%E9%98%B5-00A1E7?style=for-the-badge +[CodeStyle: black]: https://img.shields.io/badge/code%20style-black-121110.svg?style=for-the-badge +[python]: https://img.shields.io/badge/python-3.8-AB70FF?style=for-the-badge +[release]: https://img.shields.io/github/v/release/EillesWan/Musicreater?style=for-the-badge +[license]: https://img.shields.io/badge/Licence-Apache-228B22?style=for-the-badge +

音·创 Musicreater

@@ -91,10 +98,11 @@ This list is not in any order. - Thank _小埋_\ for reporting the empty add-on packs title and description problem. -
Thank 油炸 <QQ2836146704> for inspiring us to constantly develop something new.The groupmate on the picture was saying that our convert-QQ-bot had once brought him great convinience but now it closed down by some reason so he was feeling regretful."It was once, a convert-QQ-bot is just in front my eyes"
"Until lose, I finally know cannot chase back what I needs"
- Thank _雨_\ for give us report that under the new `execute` command format that the scoreboard player's add-on packs cannot play correctly. +- Thank _梦幻duang_\ for providing us with his knowlodeg of the command format in Minecraft: Java Edition Version 1.12.2. +- Thank [_Open Note Block Studio_](https://github.com/OpenNBS/NoteBlockStudio)'s Project for giving us the power and energy of continual developing. -> Thanks for the support and help of a lot of groupmates -> -> If you have given contributions but have not been in the list, please contact us! +> Thanks for the support and help of a lot of groupmates +> If you have given contributions but have not been in the list, please contact us! ## Contact Us 📞 @@ -122,9 +130,3 @@ NOT APPROVED BY OR ASSOCIATED WITH NETEASE. - 上文提及的 网易 公司,指代的是在中国大陆运营《我的世界:中国版》的上海网之易璀璨网络科技有限公司 -[Bilibili: Eilles]: https://img.shields.io/badge/Bilibili-%E9%87%91%E7%BE%BFELS-00A1E7?style=for-the-badge -[Bilibili: bgArray]: https://img.shields.io/badge/Bilibili-%E8%AF%B8%E8%91%9B%E4%BA%AE%E4%B8%8E%E5%85%AB%E5%8D%A6%E9%98%B5-00A1E7?style=for-the-badge -[CodeStyle: black]: https://img.shields.io/badge/code%20style-black-121110.svg?style=for-the-badge -[python]: https://img.shields.io/badge/python-3.8-AB70FF?style=for-the-badge -[release]: https://img.shields.io/github/v/release/EillesWan/Musicreater?style=for-the-badge -[license]: https://img.shields.io/badge/Licence-Apache-228B22?style=for-the-badge diff --git a/clean_update.py b/clean_update.py index 02e15c1..f7a1aa2 100644 --- a/clean_update.py +++ b/clean_update.py @@ -13,9 +13,7 @@ def main(): if file.endswith(".egg-info"): egg_info.append(file) console.print(file) - for file in track( - ["build", "dist", "logs", *egg_info], description="正删档" - ): + for file in track(["build", "dist", "logs", *egg_info], description="正删档"): if os.path.isdir(file) and os.access(file, os.W_OK): shutil.rmtree(file) diff --git a/docs/MSQ文件格式.md b/docs/MSQ文件格式.md index 8dfc0c5..95f48b0 100644 --- a/docs/MSQ文件格式.md +++ b/docs/MSQ文件格式.md @@ -92,11 +92,17 @@ ADD XXH64(seq_2_note_count, 3) ADD XXH64(note_seq_2, seq_2_note_count) ADD XXH128( XOR( - XXH64(meta_info, note_count), XOR( - XXH64(seq_1_note_count, 3), - XXH64(note_seq_1, seq_1_note_count) + XXH64(meta_info, note_count), + XOR( + XXH64(seq_1_note_count, 3), + XXH64(note_seq_1, seq_1_note_count) + ), ), + XOR( + XXH64(seq_2_note_count, 3), + XXH64(note_seq_2, seq_2_note_count), + ) ), note_count ) diff --git a/example.py b/example.py index 417623c..fdf651a 100644 --- a/example.py +++ b/example.py @@ -187,7 +187,7 @@ print( cvt_method( cvt_mid, out_path, - Musicreater.DEFAULT_PROGRESSBAR_STYLE if prompts[2] else None, # type: ignore + Musicreater.DEFAULT_PROGRESSBAR_STYLE if prompts[2] else None, # type: ignore *prompts[3:], ) ) diff --git a/example_msq_opera.py b/example_msq_opera.py index 124ae8d..55cafb7 100644 --- a/example_msq_opera.py +++ b/example_msq_opera.py @@ -1,14 +1,31 @@ import Musicreater +from Musicreater.utils import load_decode_msq_flush_release, load_decode_msq_metainfo + +from rich.pretty import pprint msc_seq = Musicreater.MusicSequence.from_mido( - Musicreater.mido.MidiFile("./resources/测试片段.mid",), + Musicreater.mido.MidiFile( + "./resources/测试片段.mid", + ), "TEST-测试片段", ) -with open("test.msq","wb") as f: +pprint("音乐源取入成功:") +pprint(msc_seq) + +with open("test.msq", "wb") as f: f.write(msq_bytes := msc_seq.encode_dump()) -with open("test.msq","rb") as f: +with open("test.msq", "rb") as f: msc_seq_r = Musicreater.MusicSequence.load_decode(f.read()) - -print(msc_seq_r) \ No newline at end of file + +pprint("常规 MSQ 读取成功:") +pprint(msc_seq_r) + + +with open("test.msq", "rb") as f: + pprint("流式 MSQ 元数据:") + pprint(metas := load_decode_msq_metainfo(f)) + pprint("流式 MSQ 音符序列:") + for i in load_decode_msq_flush_release(f, metas[-1], metas[-2]): + pprint(i) diff --git a/let_future_java.py b/let_future_java.py index 45e5e42..9547f86 100644 --- a/let_future_java.py +++ b/let_future_java.py @@ -6,10 +6,6 @@ Copyright © 2025 Eilles & bgArray Terms & Conditions: License.md in the root directory """ - - - - import os import shutil @@ -130,6 +126,15 @@ print( input("midi路径:"), play_speed=float(input("播放速度:")), old_exe_format=True, + note_table_replacement={ + "note.iron_xylophone": "note.xylophone", + "note.cow_bell": "note.xylophone", + "note.didgeridoo": "note.guitar", + "note.bit": "note.harp", + "note.banjo": "note.flute", + "note.pling": "note.harp", + }, + # pitched_note_table=Musicreater.MM_NBS_PITCHED_INSTRUMENT_TABLE, ), input("输出路径:"), Musicreater.experiment.ProgressBarStyle(),