diff --git a/.gitignore b/.gitignore index 7c88dd3..b1a39f9 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ /*.midi /*.mcpack /*.bdx +/*.msq /*.json /*.mcstructure .mscbackup diff --git a/Musicreater/__init__.py b/Musicreater/__init__.py index f5dd249..cfe8811 100644 --- a/Musicreater/__init__.py +++ b/Musicreater/__init__.py @@ -17,18 +17,19 @@ Terms & Conditions: License.md in the root directory # 若需转载或借鉴 许可声明请查看仓库目录下的 License.md -__version__ = "2.0.0-beta" +__version__ = "2.0.0" __vername__ = "全新组织架构" __author__ = ( ("金羿", "Eilles Wan"), ("诸葛亮与八卦阵", "bgArray"), ("偷吃不是Touch", "Touch"), - ("鸣凤鸽子", "MingFengPigeon"), + ("鱼旧梦", "ElapsingDreams"), ) __all__ = [ # 主要类 "MusicSequence", "MidiConvert", + "MusicSave", # 附加类 "SingleNote", "MineNote", diff --git a/Musicreater/constants.py b/Musicreater/constants.py index a9acce5..e24353b 100644 --- a/Musicreater/constants.py +++ b/Musicreater/constants.py @@ -467,15 +467,15 @@ MM_INSTRUMENT_DEVIATION_TABLE: Dict[str, int] = { "note.banjo": 6, "note.flute": 18, "note.bass": -18, - "note.snare": -1, + "note.snare": 0, "note.didgeridoo": -18, - "mob.zombie.wood": -1, + "mob.zombie.wood": 0, "note.bit": 6, - "note.hat": -1, - "note.bd": -1, - "firework.blast": -1, - "firework.twinkle": -1, - "fire.ignite": -1, + "note.hat": 0, + "note.bd": 0, + "firework.blast": 0, + "firework.twinkle": 0, + "fire.ignite": 0, "note.cow_bell": 6, } """不同乐器的音调偏离对照表""" diff --git a/Musicreater/experiment.py b/Musicreater/experiment.py index 20a587b..63fcea0 100644 --- a/Musicreater/experiment.py +++ b/Musicreater/experiment.py @@ -157,7 +157,7 @@ class FutureMidiConvertM4(MidiConvert): *relative_coordinates, volume_percentage, 1.0 if note.percussive else mc_pitch, - self.minium_volume, + self.minimum_volume, ) ), annotation=( diff --git a/Musicreater/main.py b/Musicreater/main.py index c886e66..63c17e5 100644 --- a/Musicreater/main.py +++ b/Musicreater/main.py @@ -88,7 +88,7 @@ class MusicSequence: note_count_per_instrument: Dict[str, int] """所使用的乐器""" - minium_volume: float + minimum_volume: float """乐曲最小音量""" music_deviation: float @@ -100,11 +100,11 @@ class MusicSequence: channels_of_notes: MineNoteChannelType, music_note_count: Optional[int] = None, note_used_per_instrument: Optional[Dict[str, int]] = None, - minium_volume_of_music: float = 0.1, + minimum_volume_of_music: float = 0.1, deviation_value: Optional[float] = None, ) -> None: """ - 《我的世界》音符序列类 + 音符序列类 Paramaters ========== @@ -112,21 +112,27 @@ class MusicSequence: 乐曲名称 channels_of_notes: MineNoteChannelType 音乐音轨 - minium_volume_of_music: float + music_note_count: int + 总音符数 + note_used_per_instrument: Dict[str, int] + 全曲乐器使用统计 + minimum_volume_of_music: float 音乐最小音量(0,1] + deviation_value: float + 全曲音调偏移值 """ - if minium_volume_of_music > 1 or minium_volume_of_music <= 0: + if minimum_volume_of_music > 1 or minimum_volume_of_music <= 0: raise IllegalMinimumVolumeError( "自订的最小音量参数错误:{},应在 (0,1] 范围内。".format( - minium_volume_of_music + minimum_volume_of_music ) ) # max_volume = 1 if max_volume > 1 else (0.001 if max_volume <= 0 else max_volume) self.music_name = name_of_music self.channels = channels_of_notes - self.minium_volume = minium_volume_of_music + self.minimum_volume = minimum_volume_of_music if (note_used_per_instrument is None) or (music_note_count is None): kp = [i.sound_name for j in self.channels.values() for i in j] @@ -151,17 +157,43 @@ class MusicSequence: midi_music_name: str, mismatch_error_ignorance: bool = True, speed_multiplier: float = 1, + default_tempo: int = mido.midifiles.midifiles.DEFAULT_TEMPO, pitched_note_referance_table: MidiInstrumentTableType = MM_TOUCH_PITCHED_INSTRUMENT_TABLE, percussion_note_referance_table: MidiInstrumentTableType = MM_TOUCH_PERCUSSION_INSTRUMENT_TABLE, - minium_vol: float = 0.1, + minimum_vol: float = 0.1, volume_processing_function: FittingFunctionType = natural_curve, - default_tempo: int = mido.midifiles.midifiles.DEFAULT_TEMPO, deviation: float = 0, ): + """ + 自mido对象导入一个音符序列类 + + Paramaters + ========== + mido_file: mido.MidiFile 对象 + 需要处理的midi对象 + midi_music_name: str + 音乐名称 + mismatch_error_ignorance bool + 是否在导入时忽略音符不匹配错误 + speed_multiplier: float + 音乐播放速度倍数 + default_tempo: int + 默认的MIDI TEMPO值 + pitched_note_referance_table: Dict[int, Tuple[str, int]] + 乐音乐器Midi-MC对照表 + percussion_note_referance_table: Dict[int, Tuple[str, int]] + 打击乐器Midi-MC对照表 + minimum_vol: float + 播放的最小音量 应为 (0,1] 范围内的小数 + volume_processing_function: Callable[[float], float] + 声像偏移拟合函数 + deviation: float + 全曲音调偏移值 + """ ( note_channels, note_count_total, - inst_note_count, # qualified_inst_note_count, + inst_note_count, ) = cls.to_music_note_channels( midi=mido_file, speed=speed_multiplier, @@ -176,22 +208,34 @@ class MusicSequence: channels_of_notes=note_channels, music_note_count=note_count_total, note_used_per_instrument=inst_note_count, - minium_volume_of_music=minium_vol, + minimum_volume_of_music=minimum_vol, deviation_value=deviation, ) def set_min_volume(self, volume_value: int): - self.minium_volume = volume_value + """重新设置全曲最小音量""" + if volume_value > 1 or volume_value <= 0: + raise IllegalMinimumVolumeError( + "自订的最小音量参数错误:{},应在 (0,1] 范围内。".format(volume_value) + ) + self.minimum_volume = volume_value def set_deviation(self, deviation_value: int): + """重新设置全曲音调偏移""" self.music_deviation = deviation_value def rename_music(self, new_name: str): + """重命名此音乐""" self.music_name = new_name def add_note(self, channel_no: int, note: MineNote, is_sort: bool = False): + """在指定通道添加一个音符""" self.channels[channel_no].append(note) self.total_note_count += 1 + if note.sound_name in self.note_count_per_instrument.keys(): + self.note_count_per_instrument[note.sound_name] += 1 + else: + self.note_count_per_instrument[note.sound_name] = 1 if is_sort: self.channels[channel_no].sort(key=lambda note: note.start_tick) @@ -247,34 +291,45 @@ class MusicSequence: midi: mido.MidiFile, ignore_mismatch_error: bool = True, speed: float = 1.0, + 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, - default_tempo_value: int = mido.midifiles.midifiles.DEFAULT_TEMPO, vol_processing_function: FittingFunctionType = natural_curve, - ) -> Tuple[MineNoteChannelType, int, Dict[str, int]]: # , Dict[str, int]]: + ) -> Tuple[MineNoteChannelType, int, Dict[str, int]]: """ 将midi解析并转换为频道音符字典 + Parameters + ---------- + midi: mido.MidiFile 对象 + 需要处理的midi对象 + ignore_mismatch_error: bool + 是否在导入时忽略音符不匹配错误 + speed: float + 音乐播放速度倍数 + 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] + 声像偏移拟合函数 + Returns ------- - 以频道作为分割的Midi音符列表字典: - Dict[int,List[SingleNote,]] + 以频道作为分割的Midi音符列表字典, 音符总数, 乐器使用统计: + Tuple[MineNoteChannelType, int, Dict[str, int]] """ if speed == 0: raise ZeroSpeedError("播放速度为 0 ,其需要(0,1]范围内的实数。") - # if midi is None: - # raise MidiUnboundError( - # "Midi参量为空。你是否正在使用的是一个由 copy_important 生成的MidiConvert对象?这是不可复用的。" - # ) - # 一个midi中仅有16个通道 我们通过通道来识别而不是音轨 midi_channels: MineNoteChannelType = empty_midi_channels(staff=[]) tempo = default_tempo_value note_count = 0 note_count_per_instrument: Dict[str, int] = {} - # qualified_note_count_per_instruments: Dict[str, int] = {} # 我们来用通道统计音乐信息 # 但是是用分轨的思路的 @@ -366,14 +421,8 @@ class MusicSequence: note_count += 1 if that_note.sound_name in note_count_per_instrument.keys(): note_count_per_instrument[that_note.sound_name] += 1 - # qualified_note_count_per_instruments[ - # that_note.sound_name - # ] += is_note_in_diapason(that_note) else: note_count_per_instrument[that_note.sound_name] = 1 - # qualified_note_count_per_instruments[ - # that_note.sound_name - # ] = int(is_note_in_diapason(that_note)) else: if ignore_mismatch_error: print( @@ -411,7 +460,6 @@ class MusicSequence: channels, note_count, note_count_per_instrument, - # qualified_note_count_per_instruments, ) @@ -442,9 +490,8 @@ class MidiConvert(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, - # enable_devation_guess: bool = True, enable_old_exe_format: bool = False, - minium_volume: float = 0.1, + minimum_volume: float = 0.1, vol_processing_function: FittingFunctionType = natural_curve, ): """ @@ -454,14 +501,24 @@ class MidiConvert(MusicSequence): ---------- midi_obj: mido.MidiFile 对象 需要处理的midi对象 - midi_name: MIDI乐曲名称 - 此音乐之名 - enable_old_exe_format: bool - 是否启用旧版(≤1.19)指令格式,默认为否 + midi_name: str + 此音乐之名称 + ignore_mismatch_error: bool + 是否在导入时忽略音符不匹配错误 + playment_speed: float + 音乐播放速度倍数 + 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对照表 + enable_old_exe_format: bool + 是否启用旧版(≤1.19)指令格式,默认为否 + minimum_volume: float + 最小播放音量 + vol_processing_function: Callable[[float], float] + 声像偏移拟合函数 """ cls.enable_old_exe_format: bool = enable_old_exe_format @@ -481,10 +538,9 @@ class MidiConvert(MusicSequence): speed_multiplier=playment_speed, pitched_note_referance_table=pitched_note_rtable, percussion_note_referance_table=percussion_note_rtable, - minium_vol=minium_volume, + minimum_vol=minimum_volume, volume_processing_function=vol_processing_function, default_tempo=default_tempo_value, - # devation_guess_enabled=enable_devation_guess, mismatch_error_ignorance=ignore_mismatch_error, ) @@ -497,7 +553,6 @@ class MidiConvert(MusicSequence): default_tempo: int = mido.midifiles.midifiles.DEFAULT_TEMPO, pitched_note_table: MidiInstrumentTableType = MM_TOUCH_PITCHED_INSTRUMENT_TABLE, percussion_note_table: MidiInstrumentTableType = MM_TOUCH_PERCUSSION_INSTRUMENT_TABLE, - # devation_guess_enabled: bool = True, old_exe_format: bool = False, min_volume: float = 0.1, vol_processing_func: FittingFunctionType = natural_curve, @@ -509,17 +564,22 @@ class MidiConvert(MusicSequence): ---------- midi_file_path: str midi文件地址 - - speed: float - 速度,注意:这里的速度指的是播放倍率,其原理为在播放音频的时候,每个音符的播放时间除以 speed + mismatch_error_ignorance bool + 是否在导入时忽略音符不匹配错误 + play_speed: float + 音乐播放速度倍数 + default_tempo: int + 默认的MIDI TEMPO值 pitched_note_table: Dict[int, Tuple[str, int]] 乐音乐器Midi-MC对照表 percussion_note_table: Dict[int, Tuple[str, int]] 打击乐器Midi-MC对照表 - enable_old_exe_format: bool + old_exe_format: bool 是否启用旧版(≤1.19)指令格式,默认为否 min_volume: float - 最小播放音量,注意:这里的音量范围为(0,1],如果超出将被处理为正确值 + 最小播放音量 + vol_processing_func: Callable[[float], float] + 声像偏移拟合函数 """ midi_music_name = os.path.splitext(os.path.basename(midi_file_path))[0].replace( @@ -539,9 +599,8 @@ class MidiConvert(MusicSequence): default_tempo_value=default_tempo, pitched_note_rtable=pitched_note_table, percussion_note_rtable=percussion_note_table, - # enable_devation_guess=devation_guess_enabled, enable_old_exe_format=old_exe_format, - minium_volume=min_volume, + minimum_volume=min_volume, vol_processing_function=vol_processing_func, ) except (ValueError, TypeError) as E: @@ -549,9 +608,6 @@ class MidiConvert(MusicSequence): except FileNotFoundError as E: raise FileNotFoundError(f"文件{midi_file_path}不存在:{E}") - # ……真的那么重要吗 - # 我又几曾何时,知道祂真的会抛下我 - def form_progress_bar( self, max_score: int, @@ -564,17 +620,15 @@ class MidiConvert(MusicSequence): Parameters ---------- max_score: int - midi的乐器ID - + 最大的积分值 scoreboard_name: str 所使用的计分板名称 - progressbar_style: ProgressBarStyle 此参数详见 ../docs/库的生成与功能文档.md#进度条自定义 Returns ------- - list[SingleCommand,] + list[MineCommand,] """ pgs_style = progressbar_style.base_style """用于被替换的进度条原始样式""" @@ -665,10 +719,6 @@ class MidiConvert(MusicSequence): ) ) - # 那是假的 - # 一切都并未留下痕迹啊 - # 那梦又是多么的真实…… - if r"%%t" in pgs_style: result.append( MineCommand( @@ -841,7 +891,7 @@ class MidiConvert(MusicSequence): Returns ------- - tuple( list[list[SingleCommand指令,... ],... ], int指令数量, int音乐时长游戏刻 ) + tuple( list[list[MineCommand指令,... ],... ], int指令数量, int音乐时长游戏刻 ) """ command_channels = [] @@ -884,7 +934,7 @@ class MidiConvert(MusicSequence): *relative_coordinates, volume_percentage, 1.0 if note.percussive else mc_pitch, - self.minium_volume, + self.minimum_volume, ) ), annotation=( @@ -923,7 +973,7 @@ class MidiConvert(MusicSequence): Returns ------- - tuple( list[SingleCommand,...], int音乐时长游戏刻, int最大同时播放的指令数量 ) + tuple( list[MineCommand指令,...], int音乐时长游戏刻, int最大同时播放的指令数量 ) """ notes_list: List[MineNote] = sorted( @@ -962,7 +1012,7 @@ class MidiConvert(MusicSequence): *relative_coordinates, volume_percentage, 1.0 if note.percussive else mc_pitch, - self.minium_volume, + self.minimum_volume, ) ), annotation=( @@ -997,7 +1047,7 @@ class MidiConvert(MusicSequence): Returns ------- - Tuple[Dict[str, List[MineCommand]], int音乐时长游戏刻, int最大同时播放的指令数量 ) + Tuple[Dict[str, List[MineCommand指令]], int音乐时长游戏刻, int最大同时播放的指令数量 ) """ notes_list: List[MineNote] = sorted( @@ -1048,7 +1098,7 @@ class MidiConvert(MusicSequence): *relative_coordinates, volume_percentage, 1.0 if note.percussive else mc_pitch, - self.minium_volume, + self.minimum_volume, ) ), annotation=( @@ -1082,3 +1132,72 @@ class MidiConvert(MusicSequence): dst.music_command_list = [i.copy() for i in self.music_command_list] dst.progress_bar_command = [i.copy() for i in self.progress_bar_command] return dst + + +class MusicSave(MusicSequence): + + @classmethod + def load_decode( + cls, + bytes_buffer_in: bytes, + ): + """从字节码导入音乐序列""" + + group_1 = int.from_bytes(bytes_buffer_in[4:6], "big") + music_name_ = bytes_buffer_in[8 : (stt_index := 8 + (group_1 >> 10))].decode( + "utf-8" + ) + channels_: MineNoteChannelType = empty_midi_channels(staff=[]) + for channel_index in channels_.keys(): + for i in range( + int.from_bytes( + bytes_buffer_in[stt_index : (stt_index := stt_index + 4)], "big" + ) + ): + try: + end_index = stt_index + 14 + (bytes_buffer_in[stt_index] >> 2) + channels_[channel_index].append( + MineNote.decode(bytes_buffer_in[stt_index:end_index]) + ) + stt_index = end_index + except: + print(channels_) + raise + + return cls( + name_of_music=music_name_, + channels_of_notes=channels_, + minimum_volume_of_music=(group_1 & 0b1111111111) / 1000, + deviation_value=int.from_bytes(bytes_buffer_in[6:8], "big", signed=True) + / 1000, + ) + + def encode_dump( + self, + ) -> bytes: + """将音乐序列转为二进制字节码""" + + # 音乐名称长度 6 位 支持到 63 + # 最小音量 minimum_volume 10 位 最大支持 1023 即三位小数 + # 共 16 位 合 2 字节 + # +++ + # 总音调偏移 music_deviation 16 位 最大支持 -32768 ~ 32767 即 三位小数 + # 共 16 位 合 2 字节 + # +++ + # 音乐名称 music_name 长度最多63 支持到 21 个中文字符 或 63 个西文字符 + bytes_buffer = ( + b"MSQ#" + + ( + (len(r := self.music_name.encode("utf-8")) << 10) + + round(self.minimum_volume * 1000) + ).to_bytes(2, "big") + + round(self.music_deviation * 1000).to_bytes(2, "big", signed=True) + + r + ) + + for channel_index, note_list in self.channels.items(): + bytes_buffer += len(note_list).to_bytes(4, "big") + for note_ in note_list: + bytes_buffer += note_.encode() + + return bytes_buffer diff --git a/Musicreater/previous.py b/Musicreater/previous.py deleted file mode 100644 index 5b266c3..0000000 --- a/Musicreater/previous.py +++ /dev/null @@ -1,483 +0,0 @@ -# -*- coding: utf-8 -*- -""" -旧版本转换功能以及已经弃用的函数 -""" - -""" -版权所有 © 2024 音·创 开发者 -Copyright © 2024 all the developers of Musicreater - -开源相关声明请见 仓库根目录下的 License.md -Terms & Conditions: License.md in the root directory -""" - -# 睿乐组织 开发交流群 861684859 -# Email TriM-Organization@hotmail.com -# 若需转载或借鉴 许可声明请查看仓库目录下的 License.md - -from .exceptions import * -from .main import MidiConvert, mido -from .subclass import * -from .types import ChannelType -from .utils import * -from .constants import * - - -class ObsoleteMidiConvert(MidiConvert): - """ - 我说一句话: - 这些破烂老代码能跑得起来就是谢天谢地,你们还指望我怎么样?这玩意真的不会再维护了,我发誓! - """ - - def to_music_channels( - self, - ) -> ChannelType: - """ - 使用金羿的转换思路,将midi解析并转换为频道信息字典 - - Returns - ------- - 以频道作为分割的Midi信息字典: - Dict[int,Dict[int,List[Union[Tuple[Literal["PgmC"], int, int],Tuple[Literal["NoteS"], int, int, int],Tuple[Literal["NoteE"], int, int],]],],] - """ - if self.midi is None: - raise MidiUnboundError( - "你是否正在使用的是一个由 copy_important 生成的MidiConvert对象?这是不可复用的。" - ) - - # 一个midi中仅有16个通道 我们通过通道来识别而不是音轨 - midi_channels: ChannelType = empty_midi_channels() - tempo = mido.midifiles.midifiles.DEFAULT_TEMPO - - # 我们来用通道统计音乐信息 - # 但是是用分轨的思路的 - for track_no, track in enumerate(self.midi.tracks): - microseconds = 0 - if not track: - continue - - note_queue = empty_midi_channels(staff=[]) - - for msg in track: - if msg.time != 0: - microseconds += msg.time * tempo / self.midi.ticks_per_beat / 1000 - - if msg.is_meta: - if msg.type == "set_tempo": - tempo = msg.tempo - else: - try: - if not track_no in midi_channels[msg.channel].keys(): - midi_channels[msg.channel][track_no] = [] - except AttributeError as E: - print(msg, E) - - if msg.type == "program_change": - midi_channels[msg.channel][track_no].append( - ("PgmC", msg.program, microseconds) - ) - - elif msg.type == "note_on" and msg.velocity != 0: - midi_channels[msg.channel][track_no].append( - ("NoteS", msg.note, msg.velocity, microseconds) - ) - - elif (msg.type == "note_on" and msg.velocity == 0) or ( - msg.type == "note_off" - ): - midi_channels[msg.channel][track_no].append( - ("NoteE", msg.note, microseconds) - ) - - """整合后的音乐通道格式 - 每个通道包括若干消息元素其中逃不过这三种: - - 1 切换乐器消息 - ("PgmC", 切换后的乐器ID: int, 距离演奏开始的毫秒) - - 2 音符开始消息 - ("NoteS", 开始的音符ID, 力度(响度), 距离演奏开始的毫秒) - - 3 音符结束消息 - ("NoteE", 结束的音符ID, 距离演奏开始的毫秒)""" - del tempo, self.channels - self.channels = midi_channels - # [print([print(no,tno,sum([True if i[0] == 'NoteS' else False for i in track])) for tno,track in cna.items()]) if cna else False for no,cna in midi_channels.items()] - return midi_channels - - def to_command_list_method1( - self, - scoreboard_name: str = "mscplay", - MaxVolume: float = 1.0, - speed: float = 1.0, - ) -> list: - """ - 使用Dislink Sforza的转换思路,将midi转换为我的世界命令列表 - :param scoreboard_name: 我的世界的计分板名称 - :param speed: 速度,注意:这里的速度指的是播放倍率,其原理为在播放音频的时候,每个音符的播放时间除以 speed - :return: tuple(命令列表, 命令个数, 计分板最大值) - """ - # :param volume: 音量,注意:这里的音量范围为(0,1],如果超出将被处理为正确值,其原理为在距离玩家 (1 / volume -1) 的地方播放音频 - tracks = [] - if speed == 0: - raise ZeroSpeedError("播放速度仅可为正实数") - if not self.midi: - raise MidiUnboundError( - "你是否正在使用的是一个由 copy_important 生成的MidiConvert对象?这是不可复用的。" - ) - - MaxVolume = 1 if MaxVolume > 1 else (0.001 if MaxVolume <= 0 else MaxVolume) - commands = 0 - maxscore = 0 - tempo = mido.midifiles.midifiles.DEFAULT_TEMPO - - # 分轨的思路其实并不好,但这个算法就是这样 - # 所以我建议用第二个方法 _toCmdList_m2 - for i, track in enumerate(self.midi.tracks): - ticks = 0 - instrumentID = 0 - singleTrack = [] - - for msg in track: - ticks += msg.time - if msg.is_meta: - if msg.type == "set_tempo": - tempo = msg.tempo - else: - if msg.type == "program_change": - instrumentID = msg.program - - if msg.type == "note_on" and msg.velocity != 0: - nowscore = round( - (ticks * tempo) - / ((self.midi.ticks_per_beat * float(speed)) * 50000) - ) - maxscore = max(maxscore, nowscore) - if msg.channel == 9: - soundID, _X = inst_to_sould_with_deviation( - instrumentID, MM_CLASSIC_PERCUSSION_INSTRUMENT_TABLE - ) - else: - soundID, _X = inst_to_sould_with_deviation( - instrumentID, MM_CLASSIC_PITCHED_INSTRUMENT_TABLE - ) - - singleTrack.append( - "execute @a[scores={" - + str(scoreboard_name) - + "=" - + str(nowscore) - + "}" - + f"] ~ ~ ~ playsound {soundID} @s ^ ^ ^{1 / MaxVolume - 1} {msg.velocity / 128} " - f"{2 ** ((msg.note - 60 - _X) / 12)}" - ) - commands += 1 - - if len(singleTrack) != 0: - tracks.append(singleTrack) - - return [tracks, commands, maxscore] - - def _toCmdList_m1( - self, scoreboardname: str = "mscplay", volume: float = 1.0, speed: float = 1.0 - ) -> list: - """ - 使用Dislink Sforza的转换思路,将midi转换为我的世界命令列表 - :param scoreboardname: 我的世界的计分板名称 - :param volume: 音量,注意:这里的音量范围为(0,1],如果超出将被处理为正确值,其原理为在距离玩家 (1 / volume -1) 的地方播放音频 - :param speed: 速度,注意:这里的速度指的是播放倍率,其原理为在播放音频的时候,每个音符的播放时间除以 speed - :return: tuple(命令列表, 命令个数, 计分板最大值) - """ - tracks = [] - if volume > 1: - volume = 1 - if volume <= 0: - volume = 0.001 - - commands = 0 - maxscore = 0 - - for i, track in enumerate(self.midi.tracks): # type:ignore - ticks = 0 - instrumentID = 0 - singleTrack = [] - - for msg in track: - ticks += msg.time - # print(msg) - if msg.is_meta: - if msg.type == "set_tempo": - tempo = msg.tempo - else: - if msg.type == "program_change": - # print("TT") - instrumentID = msg.program - if msg.type == "note_on" and msg.velocity != 0: - nowscore = round( - (ticks * tempo) / ((self.midi.ticks_per_beat * float(speed)) * 50000) # type: ignore - ) - maxscore = max(maxscore, nowscore) - if msg.channel == 9: - soundID, _X = inst_to_sould_with_deviation( - instrumentID, MM_CLASSIC_PERCUSSION_INSTRUMENT_TABLE - ) - else: - soundID, _X = inst_to_sould_with_deviation( - instrumentID, MM_CLASSIC_PITCHED_INSTRUMENT_TABLE - ) - singleTrack.append( - "execute @a[scores={" - + str(scoreboardname) - + "=" - + str(nowscore) - + "}" - + f"] ~ ~ ~ playsound {soundID} @s ~ ~{1 / volume - 1} ~ {msg.velocity * (0.7 if msg.channel == 0 else 0.9)} {2 ** ((msg.note - 60 - _X) / 12)}" - ) - commands += 1 - if len(singleTrack) != 0: - tracks.append(singleTrack) - - return [tracks, commands, maxscore] - - # 原本这个算法的转换效果应该和上面的算法相似的 - def _toCmdList_m2( - self, - scoreboard_name: str = "mscplay", - MaxVolume: float = 1.0, - speed: float = 1.0, - ) -> tuple: - """ - 使用神羽和金羿的转换思路,将midi转换为我的世界命令列表 - :param scoreboard_name: 我的世界的计分板名称 - :param MaxVolume: 音量,注意:这里的音量范围为(0,1],如果超出将被处理为正确值,其原理为在距离玩家 (1 / volume -1) 的地方播放音频 - :param speed: 速度,注意:这里的速度指的是播放倍率,其原理为在播放音频的时候,每个音符的播放时间除以 speed - :return: tuple(命令列表, 命令个数, 计分板最大值) - """ - - if speed == 0: - raise ZeroSpeedError("播放速度仅可为正实数") - MaxVolume = 1 if MaxVolume > 1 else (0.001 if MaxVolume <= 0 else MaxVolume) - - tracks = [] - cmdAmount = 0 - maxScore = 0 - InstID = -1 - - self.to_music_channels() - - # 此处 我们把通道视为音轨 - for i in self.channels.keys(): - # 如果当前通道为空 则跳过 - if not self.channels[i]: - continue - - if i == 9: - SpecialBits = True - else: - SpecialBits = False - - nowTrack = [] - - for track_no, track in self.channels[i].items(): # type: ignore - for msg in track: - if msg[0] == "PgmC": - InstID = msg[1] - - elif msg[0] == "NoteS": - soundID, _X = ( - inst_to_sould_with_deviation( - msg[1], MM_CLASSIC_PERCUSSION_INSTRUMENT_TABLE - ) - if SpecialBits - else inst_to_sould_with_deviation( - InstID, MM_CLASSIC_PITCHED_INSTRUMENT_TABLE - ) - ) - score_now = round(msg[-1] / float(speed) / 50) - maxScore = max(maxScore, score_now) - - nowTrack.append( - self.execute_cmd_head.format( - "@a[scores=({}={})]".format(scoreboard_name, score_now) - .replace("(", r"{") - .replace(")", r"}") - ) - + f"playsound {soundID} @s ^ ^ ^{1 / MaxVolume - 1} {msg[2] / 128} " - f"{2 ** ((msg[1] - 60 - _X) / 12)}" - ) - - cmdAmount += 1 - - if nowTrack: - tracks.append(nowTrack) - - return tracks, cmdAmount, maxScore - - def _toCmdList_withDelay_m1( - self, - MaxVolume: float = 1.0, - speed: float = 1.0, - player: str = "@a", - ) -> list: - """ - 使用Dislink Sforza的转换思路,将midi转换为我的世界命令列表,并输出每个音符之后的延迟 - :param MaxVolume: 最大播放音量,注意:这里的音量范围为(0,1],如果超出将被处理为正确值,其原理为在距离玩家 (1 / volume -1) 的地方播放音频 - :param speed: 速度,注意:这里的速度指的是播放倍率,其原理为在播放音频的时候,每个音符的播放时间除以 speed - :param player: 玩家选择器,默认为`@a` - :return: 全部指令列表[ ( str指令, int距离上一个指令的延迟 ),...] - """ - tracks = {} - - if speed == 0: - raise ZeroSpeedError("播放速度仅可为正实数") - if not self.midi: - raise MidiUnboundError( - "你是否正在使用的是一个由 copy_important 生成的MidiConvert对象?这是不可复用的。" - ) - - MaxVolume = 1 if MaxVolume > 1 else (0.001 if MaxVolume <= 0 else MaxVolume) - tempo = mido.midifiles.midifiles.DEFAULT_TEMPO - - for i, track in enumerate(self.midi.tracks): - instrumentID = 0 - ticks = 0 - - for msg in track: - ticks += msg.time - if msg.is_meta: - if msg.type == "set_tempo": - tempo = msg.tempo - else: - if msg.type == "program_change": - instrumentID = msg.program - if msg.type == "note_on" and msg.velocity != 0: - now_tick = round( - (ticks * tempo) - / ((self.midi.ticks_per_beat * float(speed)) * 50000) - ) - - if msg.channel == 9: - soundID, _X = inst_to_sould_with_deviation( - instrumentID, MM_CLASSIC_PERCUSSION_INSTRUMENT_TABLE - ) - else: - soundID, _X = inst_to_sould_with_deviation( - instrumentID, MM_CLASSIC_PITCHED_INSTRUMENT_TABLE - ) - try: - tracks[now_tick].append( - self.execute_cmd_head.format(player) - + f"playsound {soundID} @s ^ ^ ^{1 / MaxVolume - 1} {msg.velocity / 128} " - f"{2 ** ((msg.note - 60 - _X) / 12)}" - ) - except KeyError: - tracks[now_tick] = [ - self.execute_cmd_head.format(player) - + f"playsound {soundID} @s ^ ^ ^{1 / MaxVolume - 1} {msg.velocity / 128} " - f"{2 ** ((msg.note - 60 - _X) / 12)}" - ] - - results = [] - - all_ticks = list(tracks.keys()) - all_ticks.sort() - - for i in range(len(all_ticks)): - if i != 0: - for j in range(len(tracks[all_ticks[i]])): - if j != 0: - results.append((tracks[all_ticks[i]][j], 0)) - else: - results.append( - (tracks[all_ticks[i]][j], all_ticks[i] - all_ticks[i - 1]) - ) - else: - for j in range(len(tracks[all_ticks[i]])): - results.append((tracks[all_ticks[i]][j], all_ticks[i])) - - return [results, max(all_ticks)] - - def _toCmdList_withDelay_m2( - self, - MaxVolume: float = 1.0, - speed: float = 1.0, - player: str = "@a", - ) -> list: - """ - 使用神羽和金羿的转换思路,将midi转换为我的世界命令列表,并输出每个音符之后的延迟 - :param MaxVolume: 最大播放音量,注意:这里的音量范围为(0,1],如果超出将被处理为正确值,其原理为在距离玩家 (1 / volume -1) 的地方播放音频 - :param speed: 速度,注意:这里的速度指的是播放倍率,其原理为在播放音频的时候,每个音符的播放时间除以 speed - :param player: 玩家选择器,默认为`@a` - :return: 全部指令列表[ ( str指令, int距离上一个指令的延迟 ),...] - """ - tracks = {} - if speed == 0: - raise ZeroSpeedError("播放速度仅可为正实数") - - MaxVolume = 1 if MaxVolume > 1 else (0.001 if MaxVolume <= 0 else MaxVolume) - InstID = -1 - self.to_music_channels() - - results = [] - - for i in self.channels.keys(): - # 如果当前通道为空 则跳过 - if not self.channels[i]: - continue - - if i == 9: - SpecialBits = True - else: - SpecialBits = False - - for track_no, track in self.channels[i].items(): # type: ignore - for msg in track: - if msg[0] == "PgmC": - InstID = msg[1] - - elif msg[0] == "NoteS": - soundID, _X = ( - inst_to_sould_with_deviation( - msg[1], MM_CLASSIC_PERCUSSION_INSTRUMENT_TABLE - ) - if SpecialBits - else inst_to_sould_with_deviation( - InstID, MM_CLASSIC_PITCHED_INSTRUMENT_TABLE - ) - ) - score_now = round(msg[-1] / float(speed) / 50) - - try: - tracks[score_now].append( - self.execute_cmd_head.format(player) - + f"playsound {soundID} @s ^ ^ ^{1 / MaxVolume - 1} {msg[2] / 128} " - f"{2 ** ((msg[1] - 60 - _X) / 12)}" - ) - except KeyError: - tracks[score_now] = [ - self.execute_cmd_head.format(player) - + f"playsound {soundID} @s ^ ^ ^{1 / MaxVolume - 1} {msg[2] / 128} " - f"{2 ** ((msg[1] - 60 - _X) / 12)}" - ] - - all_ticks = list(tracks.keys()) - all_ticks.sort() - - for i in range(len(all_ticks)): - for j in range(len(tracks[all_ticks[i]])): - results.append( - ( - tracks[all_ticks[i]][j], - ( - 0 - if j != 0 - else ( - all_ticks[i] - all_ticks[i - 1] - if i != 0 - else all_ticks[i] - ) - ), - ) - ) - - return [results, max(all_ticks)] diff --git a/Musicreater/subclass.py b/Musicreater/subclass.py index d6e5892..53b6970 100644 --- a/Musicreater/subclass.py +++ b/Musicreater/subclass.py @@ -104,10 +104,124 @@ class MineNote: self.extra_info = extra_information - # @property - # def get_mc_pitch(self,table: Dict[int, Tuple[str, int]]) -> float: - # self.mc_sound_ID, _X = inst_to_sould_with_deviation(self.inst,table,"note.bd" if self.percussive else "note.flute",) - # return -1 if self.percussive else 2 ** ((self.note - 60 - _X) / 12) + @classmethod + def decode(cls, code_buffer: bytes): + """自字节码析出MineNote类""" + group_1 = int.from_bytes(code_buffer[:6], "big") + percussive_ = bool(group_1 & 0b1) + duration_ = (group_1 := group_1 >> 1) & 0b11111111111111111 + start_tick_ = (group_1 := group_1 >> 17) & 0b11111111111111111 + note_pitch_ = (group_1 := group_1 >> 17) & 0b1111111 + sound_name_length = group_1 >> 7 + + if code_buffer[6] & 0b1: + position_displacement_ = ( + int.from_bytes( + code_buffer[8 + sound_name_length : 10 + sound_name_length], + "big", + ) + / 1000, + int.from_bytes( + code_buffer[10 + sound_name_length : 12 + sound_name_length], + "big", + ) + / 1000, + int.from_bytes( + code_buffer[12 + sound_name_length : 14 + sound_name_length], + "big", + ) + / 1000, + ) + else: + position_displacement_ = (0, 0, 0) + + try: + return cls( + mc_sound_name=code_buffer[8 : 8 + sound_name_length].decode( + encoding="utf-8" + ), + midi_pitch=note_pitch_, + midi_velocity=code_buffer[6] >> 1, + start_time=start_tick_, + last_time=duration_, + track_number=code_buffer[7], + is_percussion=percussive_, + displacement=position_displacement_, + ) + except: + print(code_buffer, "\n", code_buffer[8 : 8 + sound_name_length]) + raise + + def encode(self, is_displacement_included: bool = True) -> bytes: + """ + 将数据打包为字节码 + + :param is_displacement_included:`bool` 是否包含声像偏移数据,默认为**是** + + :return bytes 打包好的字节码 + """ + + # 字符串长度 6 位 支持到 63 + # note_pitch 7 位 支持到 127 + # start_tick 17 位 支持到 131071 即 109.22583 分钟 合 1.8204305 小时 + # duration 17 位 支持到 131071 即 109.22583 分钟 合 1.8204305 小时 + # percussive 长度 1 位 支持到 1 + # 共 48 位 合 6 字节 + # +++ + # velocity 长度 7 位 支持到 127 + # is_displacement_included 长度 1 位 支持到 1 + # 共 8 位 合 1 字节 + # +++ + # track_no 长度 8 位 支持到 255 合 1 字节 + # +++ + # sound_name 长度最多63 支持到 21 个中文字符 或 63 个西文字符 + # +++ + # position_displacement 每个元素长 16 位 合 2 字节 + # 共 48 位 合 6 字节 支持存储三位小数和两位整数,其值必须在 [0, 65.535] 之间 + + return ( + ( + ( + ( + ( + ( + ( + ( + ( + len( + r := self.sound_name.encode( + encoding="utf-8" + ) + ) + << 7 + ) + + self.note_pitch + ) + << 17 + ) + + self.start_tick + ) + << 17 + ) + + self.duration + ) + << 1 + ) + + self.percussive + ).to_bytes(6, "big") + + ((self.velocity << 1) + is_displacement_included).to_bytes(1, "big") + + self.track_no.to_bytes(1, "big") + + r + + ( + ( + round(self.position_displacement[0] * 1000).to_bytes(2, "big") + + round(self.position_displacement[1] * 1000).to_bytes(2, "big") + + round(self.position_displacement[2] * 1000).to_bytes(2, "big") + ) + if is_displacement_included + else b"" + ) + ) def set_info(self, sth: Any): """设置附加信息""" diff --git a/Musicreater/types.py b/Musicreater/types.py index 0e354d2..37e8d5f 100644 --- a/Musicreater/types.py +++ b/Musicreater/types.py @@ -42,7 +42,9 @@ Midi乐器对照表类型 """ FittingFunctionType = Callable[[float], float] - +""" +声像偏移音量拟合函数类型 +""" ChannelType = Dict[ int, diff --git a/Musicreater/utils.py b/Musicreater/utils.py index ee11a54..8229500 100644 --- a/Musicreater/utils.py +++ b/Musicreater/utils.py @@ -74,10 +74,12 @@ def inst_to_sould_with_deviation( midi的乐器ID reference_table: Dict[int, Tuple[str, int]] 转换乐器参照表 + default_instrument: str + 查无此乐器时的替换乐器 Returns ------- - tuple(str我的世界乐器名, int转换算法中的X) + tuple(str我的世界乐器名, int转换算法中的偏移量) """ sound_id = midi_inst_to_mc_sound( instrumentID=instrumentID, @@ -106,6 +108,8 @@ def midi_inst_to_mc_sound( midi的乐器ID reference_table: Dict[int, Tuple[str, int]] 转换乐器参照表 + default_instrument: str + 查无此乐器时的替换乐器 Returns ------- @@ -274,14 +278,14 @@ def midi_msgs_to_minenote( mc_distance_volume = volume_processing_method_(velocity_) return MineNote( - mc_sound_ID, - note_, - velocity_, - round(start_time_ / float(play_speed) / 50), - round(duration_ / float(play_speed) / 50), - track_no_, - percussive_, - (0, mc_distance_volume, 0), + mc_sound_name=mc_sound_ID, + midi_pitch=note_, + midi_velocity=velocity_, + start_time=round(start_time_ / float(play_speed) / 50), + last_time=round(duration_ / float(play_speed) / 50), + track_number=track_no_, + is_percussion=percussive_, + displacement=(0, mc_distance_volume, 0), )