diff --git a/Musicreater/__init__.py b/Musicreater/__init__.py index c0f6df2..b3efed4 100644 --- a/Musicreater/__init__.py +++ b/Musicreater/__init__.py @@ -17,8 +17,8 @@ Terms & Conditions: License.md in the root directory # 若需转载或借鉴 许可声明请查看仓库目录下的 License.md -__version__ = "1.8" -__vername__ = "音调总体偏移的处理支持" +__version__ = "2.0.0-alpha" +__vername__ = "全新组织架构" __author__ = ( ("金羿", "Eilles Wan"), ("诸葛亮与八卦阵", "bgArray"), @@ -31,7 +31,8 @@ __all__ = [ "MidiConvert", # 附加类 "SingleNote", - "SingleCommand", + "MineNote", + "MineCommand", "SingleNoteBox", # "TimeStamp", 未来功能 # 默认值 diff --git a/Musicreater/constants.py b/Musicreater/constants.py index 31f3074..9d3772c 100644 --- a/Musicreater/constants.py +++ b/Musicreater/constants.py @@ -414,28 +414,28 @@ MC_INSTRUMENT_BLOCKS_TABLE: Dict[str, Tuple[str, ...]] = { # Midi对MC通用对照表 -MM_INSTRUMENT_RANGE_TABLE: Dict[str, Tuple[int, int]] = { - "note.harp": (42, 66), - "note.pling": (42, 66), - "note.guitar": (30, 54), - "note.iron_xylophone": (42, 66), - "note.bell": (66, 90), - "note.xylophone": (66, 90), - "note.chime": (66, 90), - "note.banjo": (42, 66), - "note.flute": (54, 78), - "note.bass": (18, 42), - "note.snare": (-1, 128), # 实际上是 0~127 - "note.didgeridoo": (18, 42), - "mob.zombie.wood": (-1, 128), - "note.bit": (42, 66), - "note.hat": (-1, 128), - "note.bd": (-1, 128), - "note.basedrum": (-1, 128), - "firework.blast": (-1, 128), - "firework.twinkle": (-1, 128), - "fire.ignite": (-1, 128), - "note.cow_bell": (54, 78), +MM_INSTRUMENT_RANGE_TABLE: Dict[str, Tuple[Tuple[int, int], int]] = { + "note.harp": ((42, 66), 54), + "note.pling": ((42, 66), 54), + "note.guitar": ((30, 54), 42), + "note.iron_xylophone": ((42, 66), 54), + "note.bell": ((66, 90), 78), + "note.xylophone": ((66, 90), 78), + "note.chime": ((66, 90), 78), + "note.banjo": ((42, 66), 54), + "note.flute": ((54, 78), 66), + "note.bass": ((18, 42), 30), + "note.snare": ((-1, 128), 0), # 实际上是 0~127 + "note.didgeridoo": ((18, 42), 30), + "mob.zombie.wood": ((-1, 128), 0), + "note.bit": ((42, 66), 54), + "note.hat": ((-1, 128), 0), + "note.bd": ((-1, 128), 0), + "note.basedrum": ((-1, 128), 0), + "firework.blast": ((-1, 128), 0), + "firework.twinkle": ((-1, 128), 0), + "fire.ignite": ((-1, 128), 0), + "note.cow_bell": ((54, 78), 66), } """不同乐器的音域偏离对照表""" diff --git a/Musicreater/exceptions.py b/Musicreater/exceptions.py index cf39919..70f5774 100644 --- a/Musicreater/exceptions.py +++ b/Musicreater/exceptions.py @@ -115,3 +115,11 @@ class ZeroSpeedError(ZeroDivisionError): def __init__(self, *args): """以0作为播放速度的错误""" super().__init__("播放速度为0", *args) + + +class IllegalMinimumVolumeError(ValueError): + """最小播放音量有误的错误""" + + def __init__(self, *args): + """最小播放音量错误""" + super().__init__("最小播放音量超出范围", *args) \ No newline at end of file diff --git a/Musicreater/experiment.py b/Musicreater/experiment.py index 43ef3c5..6d267a6 100644 --- a/Musicreater/experiment.py +++ b/Musicreater/experiment.py @@ -95,19 +95,13 @@ class FutureMidiConvertM4(MidiConvert): def to_command_list_in_delay( self, - max_volume: float = 1.0, - speed: float = 1.0, player_selector: str = "@a", - ) -> Tuple[List[SingleCommand], int, int]: + ) -> Tuple[List[MineCommand], int, int]: """ 将midi转换为我的世界命令列表,并输出每个音符之后的延迟 Parameters ---------- - max_volume: float - 最大播放音量,注意:这里的音量范围为(0,1],如果超出将被处理为正确值,其原理为在距离玩家 (1 / volume -1) 的地方播放音频 - speed: float - 速度,注意:这里的速度指的是播放倍率,其原理为在播放音频的时候,每个音符的播放时间除以 speed player_selector: str 玩家选择器,默认为`@a` @@ -116,17 +110,13 @@ class FutureMidiConvertM4(MidiConvert): tuple( list[SingleCommand,...], int音乐时长游戏刻, int最大同时播放的指令数量 ) """ - if speed == 0: - raise ZeroSpeedError("播放速度仅可为(0,1]范围内的正实数") - max_volume = 1 if max_volume > 1 else (0.001 if max_volume <= 0 else max_volume) - - notes_list: List[SingleNote] = [] + notes_list: List[MineNote] = [] # 此处 我们把通道视为音轨 for channel in self.channels.values(): for note in channel: note.set_info( - note_to_command_parameters( + single_note_to_note_parameters( note, ( self.percussion_note_referrence_table @@ -160,7 +150,7 @@ class FutureMidiConvertM4(MidiConvert): max_multi = max(max_multi, multi) multi = 0 self.music_command_list.append( - SingleCommand( + MineCommand( self.execute_cmd_head.format(player_selector) + r"playsound {} @s ^ ^ ^{} {} {}".format(*note.extra_info), tick_delay=tickdelay, @@ -173,8 +163,7 @@ class FutureMidiConvertM4(MidiConvert): ) delaytime_previous = delaytime_now - self.music_tick_num = round(notes_list[-1].start_time / speed / 50) - return self.music_command_list, self.music_tick_num, max_multi + 1 + return self.music_command_list, round(notes_list[-1].start_time / speed / 50), max_multi + 1 class FutureMidiConvertM5(MidiConvert): @@ -267,7 +256,7 @@ class FutureMidiConvertM5(MidiConvert): max_volume: float = 1.0, speed: float = 1.0, player_selector: str = "@a", - ) -> Tuple[List[SingleCommand], int]: + ) -> Tuple[List[MineCommand], int]: """ 使用金羿的转换思路,使用同刻偏移算法优化音感后,将midi转换为我的世界命令列表,并输出每个音符之后的延迟 @@ -314,11 +303,11 @@ class FutureMidiConvertM5(MidiConvert): elif msg[0] == "NoteS": soundID = ( - midi_inst_to_mc_sould( + midi_inst_to_mc_sound( msg[1], MM_CLASSIC_PERCUSSION_INSTRUMENT_TABLE ) if SpecialBits - else midi_inst_to_mc_sould( + else midi_inst_to_mc_sound( InstID, MM_CLASSIC_PITCHED_INSTRUMENT_TABLE ) ) @@ -354,7 +343,7 @@ class FutureMidiConvertM5(MidiConvert): for i in range(len(all_ticks)): for j in range(len(tracks[all_ticks[i]])): results.append( - SingleCommand( + MineCommand( tracks[all_ticks[i]][j], tick_delay=( ( @@ -402,8 +391,7 @@ class FutureMidiConvertM5(MidiConvert): ) self.music_command_list = results - self.music_tick_num = max(all_ticks) - return results, self.music_tick_num + return results, max(all_ticks) class FutureMidiConvertM6(MidiConvert): diff --git a/Musicreater/main.py b/Musicreater/main.py index 4fa6b74..8bf0720 100644 --- a/Musicreater/main.py +++ b/Musicreater/main.py @@ -3,9 +3,9 @@ """ 音·创 (Musicreater) -是一款免费开源的针对《我的世界》的midi音乐转换库 +一款免费开源的针对《我的世界》音乐的支持库 Musicreater (音·创) -A free open source library used for convert midi file into formats that is suitable for **Minecraft**. +A free open source library used for **Minecraft** musics. 版权所有 © 2024 音·创 开发者 Copyright © 2024 all the developers of Musicreater @@ -19,10 +19,10 @@ Terms & Conditions: License.md in the root directory # 若需转载或借鉴 许可声明请查看仓库根目录下的 License.md -# BUG退散!BUG退散! BUG退散!BUG退散! -# 异常、错误作乱之时 異常、誤りが、困った時は -# 二六字组!万国码合!二六字组!万国码合! グループ!コード#!グループ!コード#! -# 赶快呼叫 程序员!Let's Go! 直ぐに呼びましょプログラマ レッツゴー! +# BUG退散!BUG退散! BUG退散!BUG退散! BUG retreat! BUG retreat! +# 异常与错误作乱之时 異常、誤りが、困った時は Abnormalities and errors are causing chaos +# 二六字组!万国码合!二六字组!万国码合! グループ!コード#!グループ!コード#! Words combine! Unicode unite! +# 赶快呼叫 程序员!Let's Go! 直ぐに呼びましょプログラマ レッツゴー! Hurry to call the programmer! Let's Go! import math @@ -84,49 +84,192 @@ class MusicSequence: """ music_name: str - """Midi乐曲名""" + """乐曲名""" - channels: NoteChannelType + channels: MineNoteChannelType """频道信息字典""" + total_note_count: int + """音符总数""" + + used_instrument: List[str] + """所使用的乐器""" + + minium_volume: float + """乐曲最小音量""" + + music_deviation: float + """乐曲音调偏移""" + def __init__( self, name_of_music: str, - channels_of_notes: NoteChannelType, + channels_of_notes: MineNoteChannelType, + music_note_count: Optional[int] = None, + used_instrument_of_music: Optional[List[str]] = None, + minium_volume_of_music: float = 0.1, + deviation: Optional[float] = None, ) -> None: + """ + 《我的世界》音符序列类 + + Paramaters + ========== + name_of_music: str + 乐曲名称 + channels_of_notes: MineNoteChannelType + 音乐音轨 + minium_volume_of_music: float + 音乐最小音量(0,1] + """ + + if minium_volume_of_music > 1 or minium_volume_of_music <= 0: + raise IllegalMinimumVolumeError( + "自订的最小音量参数错误:{},应在 (0,1] 范围内。".format( + minium_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 + + if used_instrument_of_music is None or music_note_count is None: + kp = [i.sound_name for j in self.channels.values() for i in j] + self.total_note_count = ( + len(kp) if music_note_count is None else music_note_count + ) + self.used_instrument = ( + list(set(kp)) + if used_instrument_of_music is None + else used_instrument_of_music + ) + + self.music_deviation = ( + self.guess_deviation( + self.total_note_count, + len(self.used_instrument), + music_channels=self.channels, + ) + if deviation is None + else deviation + ) @classmethod def from_mido( cls, mido_file: mido.MidiFile, - midi_music_name, - default_tempo: int = mido.midifiles.midifiles.DEFAULT_TEMPO, + midi_music_name: str, mismatch_error_ignorance: bool = True, + speed_multiplier: float = 1, + 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, + volume_processing_function: FittingFunctionType = natural_curve, + default_tempo: int = mido.midifiles.midifiles.DEFAULT_TEMPO, + devation_guess_enabled: bool = True, ): - - return cls( - midi_music_name, + note_channels, note_count_total, inst_note_count, qualified_inst_note_count = ( cls.to_music_note_channels( midi=mido_file, + speed=speed_multiplier, + pitched_note_rtable=pitched_note_referance_table, + percussion_note_rtable=percussion_note_referance_table, default_tempo_value=default_tempo, + vol_processing_function=volume_processing_function, ignore_mismatch_error=mismatch_error_ignorance, + ) + ) + return cls( + name_of_music=midi_music_name, + channels_of_notes=note_channels, + music_note_count=note_count_total, + used_instrument_of_music=list(inst_note_count.keys()), + minium_volume_of_music=minium_vol, + deviation=( + cls.guess_deviation( + note_count_total, + len(inst_note_count), + inst_note_count, + qualified_inst_note_count, + ) + if devation_guess_enabled + else 0 ), ) + def set_min_volume(self, volume_value: int): + self.minium_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 is_sort: + self.channels[channel_no].sort(key=lambda note: note.start_tick) + + @staticmethod + def guess_deviation( + total_note_count: int, + total_instrument_count: int, + note_count_per_instruments: Optional[Dict[str, int]] = None, + qualified_note_count_per_instruments: Optional[Dict[str, int]] = None, + music_channels: Optional[MineNoteChannelType] = None, + ) -> float: + if ( + note_count_per_instruments is None + or qualified_note_count_per_instruments is None + ): + if music_channels is None: + raise ValueError("参数不足,算逑!") + note_count_per_instruments = {} + qualified_note_count_per_instruments = {} + for this_note in [k for j in music_channels.values() for k in j]: + if this_note.sound_name in note_count_per_instruments.keys(): + note_count_per_instruments[this_note.sound_name] += 1 + qualified_note_count_per_instruments[ + this_note.sound_name + ] += is_note_in_diapason(this_note) + else: + note_count_per_instruments[this_note.sound_name] = 1 + qualified_note_count_per_instruments[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_instruments[inst]) + ) + for inst, note_count in note_count_per_instruments.items() + ] + ) + / total_instrument_count + / total_note_count + ) + @staticmethod def to_music_note_channels( midi: mido.MidiFile, - default_tempo_value: int = mido.midifiles.midifiles.DEFAULT_TEMPO, ignore_mismatch_error: bool = True, - ) -> NoteChannelType: + speed: float = 1.0, + 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]]: """ 将midi解析并转换为频道音符字典 @@ -136,14 +279,20 @@ class MusicSequence: Dict[int,List[SingleNote,]] """ + if speed == 0: + raise ZeroSpeedError("播放速度为 0 ,其需要(0,1]范围内的实数。") + # if midi is None: # raise MidiUnboundError( # "Midi参量为空。你是否正在使用的是一个由 copy_important 生成的MidiConvert对象?这是不可复用的。" # ) # 一个midi中仅有16个通道 我们通过通道来识别而不是音轨 - midi_channels: NoteChannelType = empty_midi_channels(staff=[]) + midi_channels: MineNoteChannelType = empty_midi_channels(staff=[]) tempo = default_tempo_value + note_count = 0 + note_count_per_instruments: Dict[str, int] = {} + qualified_note_count_per_instruments: Dict[str, int] = {} # 我们来用通道统计音乐信息 # 但是是用分轨的思路的 @@ -205,27 +354,47 @@ class MusicSequence: (msg.note, channel_program[msg.channel]) ) note_queue_B[msg.channel].remove((_velocity, _ms)) + midi_channels[msg.channel].append( - SingleNote( - instrument=msg.note, - pitch=channel_program[msg.channel], - velocity=_velocity, - startime=_ms, - lastime=microseconds - _ms, - track_number=track_no, - is_percussion=True, - ) - if msg.channel == 9 - else SingleNote( - instrument=channel_program[msg.channel], - pitch=msg.note, - velocity=_velocity, - startime=_ms, - lastime=microseconds - _ms, - track_number=track_no, - is_percussion=False, + that_note := midi_msgs_to_minenote( + inst_=( + msg.note + if msg.channel == 9 + else channel_program[msg.channel] + ), + note_=( + channel_program[msg.channel] + if msg.channel == 9 + else msg.note + ), + velocity_=_velocity, + start_time_=_ms, + duration_=microseconds - _ms, + track_no_=track_no, + percussive_=(msg.channel == 9), + play_speed=speed, + midi_reference_table=( + percussion_note_rtable + if msg.channel == 9 + else pitched_note_rtable + ), + volume_processing_method_=vol_processing_function, ) ) + note_count += 1 + if ( + that_note.sound_name + in note_count_per_instruments.keys() + ): + note_count_per_instruments[that_note.sound_name] += 1 + qualified_note_count_per_instruments[ + that_note.sound_name + ] += is_note_in_diapason(that_note) + else: + note_count_per_instruments[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( @@ -254,12 +423,17 @@ class MusicSequence: del tempo channels = dict( [ - (channel_no, sorted(channel_notes, key=lambda note: note.start_time)) + (channel_no, sorted(channel_notes, key=lambda note: note.start_tick)) for channel_no, channel_notes in midi_channels.items() ] ) - return channels + return ( + channels, + note_count, + note_count_per_instruments, + qualified_note_count_per_instruments, + ) class MidiConvert(MusicSequence): @@ -267,42 +441,31 @@ class MidiConvert(MusicSequence): 将Midi文件转换为我的世界内容 """ - pitched_note_reference_table: MidiInstrumentTableType - """乐音乐器Midi-MC对照表""" - - percussion_note_referrence_table: MidiInstrumentTableType - """打击乐器Midi-MC对照表""" - - volume_processing_function: FittingFunctionType - """音量处理函数""" - enable_old_exe_format: bool """是否启用旧版execute指令格式""" execute_cmd_head: str """execute指令头部""" - music_command_list: List[SingleCommand] + music_command_list: List[MineCommand] """音乐指令列表""" - music_tick_num: int - """音乐总延迟""" - - progress_bar_command: List[SingleCommand] + progress_bar_command: List[MineCommand] """进度条指令列表""" - music_deviation: int - """音乐音调总偏移""" - - def __init__( - self, + @classmethod + def from_mido_obj( + cls, midi_obj: mido.MidiFile, midi_name: str, - deviation: Union[int, Literal[None]] = 0, ignore_mismatch_error: bool = True, - enable_old_exe_format: bool = False, + playment_speed: float = 1, + 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, vol_processing_function: FittingFunctionType = natural_curve, ): """ @@ -322,40 +485,42 @@ class MidiConvert(MusicSequence): 打击乐器Midi-MC对照表 """ - super(MidiConvert, self).from_mido( - midi_obj, - midi_name, - mismatch_error_ignorance=ignore_mismatch_error, - ) + cls.enable_old_exe_format: bool = enable_old_exe_format - self.music_deviation = ( - self.guess_deviation() if deviation is None else deviation - ) - - self.enable_old_exe_format: bool = enable_old_exe_format - - self.execute_cmd_head = ( + cls.execute_cmd_head = ( "execute {} ~ ~ ~ " if enable_old_exe_format else "execute as {} at @s positioned ~ ~ ~ run " ) - self.pitched_note_reference_table = pitched_note_rtable - self.percussion_note_referrence_table = percussion_note_rtable - self.volume_processing_function = vol_processing_function + cls.progress_bar_command = cls.music_command_list = [] + cls.channels = {} - self.progress_bar_command = self.music_command_list = [] - self.channels = {} - self.music_tick_num = 0 + return cls.from_mido( + mido_file=midi_obj, + midi_music_name=midi_name, + speed_multiplier=playment_speed, + pitched_note_referance_table=pitched_note_rtable, + percussion_note_referance_table=percussion_note_rtable, + minium_vol=minium_volume, + volume_processing_function=vol_processing_function, + default_tempo=default_tempo_value, + devation_guess_enabled=enable_devation_guess, + mismatch_error_ignorance=ignore_mismatch_error, + ) @classmethod def from_midi_file( cls, midi_file_path: str, - ignore_mismatch_error: bool = True, - old_exe_format: bool = False, + mismatch_error_ignorance: bool = True, + play_speed: float = 1, + 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, ): """ @@ -363,14 +528,19 @@ class MidiConvert(MusicSequence): Parameters ---------- - midi_file: str + midi_file_path: str midi文件地址 - enable_old_exe_format: bool - 是否启用旧版(≤1.19)指令格式,默认为否 + + speed: float + 速度,注意:这里的速度指的是播放倍率,其原理为在播放音频的时候,每个音符的播放时间除以 speed 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 + 是否启用旧版(≤1.19)指令格式,默认为否 + min_volume: float + 最小播放音量,注意:这里的音量范围为(0,1],如果超出将被处理为正确值 """ midi_music_name = os.path.splitext(os.path.basename(midi_file_path))[0].replace( @@ -379,18 +549,21 @@ class MidiConvert(MusicSequence): """文件名,不含路径且不含后缀""" try: - return cls( - mido.MidiFile( + return cls.from_mido_obj( + midi_obj=mido.MidiFile( midi_file_path, clip=True, ), - midi_music_name, - None, - old_exe_format, - ignore_mismatch_error, - pitched_note_table, - percussion_note_table, - vol_processing_func, + midi_name=midi_music_name, + ignore_mismatch_error=mismatch_error_ignorance, + playment_speed=play_speed, + 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, + vol_processing_function=vol_processing_func, ) except (ValueError, TypeError) as E: raise MidiDestroyedError(f"文件{midi_file_path}损坏:{E}") @@ -400,17 +573,12 @@ class MidiConvert(MusicSequence): # ……真的那么重要吗 # 我又几曾何时,知道祂真的会抛下我 - def guess_deviation(self) -> int: - - # 等我想想看 - return 0 - def form_progress_bar( self, max_score: int, scoreboard_name: str, progressbar_style: ProgressBarStyle = DEFAULT_PROGRESSBAR_STYLE, - ) -> List[SingleCommand]: + ) -> List[MineCommand]: """ 生成进度条 @@ -446,7 +614,7 @@ class MidiConvert(MusicSequence): perEach = max_score / pgs_style.count("_") """每个进度条代表的分值""" - result: List[SingleCommand] = [] + result: List[MineCommand] = [] if r"%^s" in pgs_style: pgs_style = pgs_style.replace(r"%^s", str(max_score)) @@ -457,7 +625,7 @@ class MidiConvert(MusicSequence): sbn_pc = scoreboard_name[:2] if r"%%%" in pgs_style: result.append( - SingleCommand( + MineCommand( 'scoreboard objectives add {}PercT dummy "百分比计算"'.format( sbn_pc ), @@ -465,7 +633,7 @@ class MidiConvert(MusicSequence): ) ) result.append( - SingleCommand( + MineCommand( self.execute_cmd_head.format( "@a[scores={" + scoreboard_name + "=1..}]" ) @@ -476,7 +644,7 @@ class MidiConvert(MusicSequence): ) ) result.append( - SingleCommand( + MineCommand( self.execute_cmd_head.format( "@a[scores={" + scoreboard_name + "=1..}]" ) @@ -485,7 +653,7 @@ class MidiConvert(MusicSequence): ) ) result.append( - SingleCommand( + MineCommand( self.execute_cmd_head.format( "@a[scores={" + scoreboard_name + "=1..}]" ) @@ -496,7 +664,7 @@ class MidiConvert(MusicSequence): ) ) result.append( - SingleCommand( + MineCommand( self.execute_cmd_head.format( "@a[scores={" + scoreboard_name + "=1..}]" ) @@ -507,7 +675,7 @@ class MidiConvert(MusicSequence): ) ) result.append( - SingleCommand( + MineCommand( self.execute_cmd_head.format( "@a[scores={" + scoreboard_name + "=1..}]" ) @@ -524,7 +692,7 @@ class MidiConvert(MusicSequence): if r"%%t" in pgs_style: result.append( - SingleCommand( + MineCommand( 'scoreboard objectives add {}TMinT dummy "时间计算:分"'.format( sbn_pc ), @@ -532,7 +700,7 @@ class MidiConvert(MusicSequence): ) ) result.append( - SingleCommand( + MineCommand( 'scoreboard objectives add {}TSecT dummy "时间计算:秒"'.format( sbn_pc ), @@ -540,7 +708,7 @@ class MidiConvert(MusicSequence): ) ) result.append( - SingleCommand( + MineCommand( self.execute_cmd_head.format( "@a[scores={" + scoreboard_name + "=1..}]" ) @@ -549,7 +717,7 @@ class MidiConvert(MusicSequence): ) ) result.append( - SingleCommand( + MineCommand( self.execute_cmd_head.format( "@a[scores={" + scoreboard_name + "=1..}]" ) @@ -559,7 +727,7 @@ class MidiConvert(MusicSequence): ) result.append( - SingleCommand( + MineCommand( self.execute_cmd_head.format( "@a[scores={" + scoreboard_name + "=1..}]" ) @@ -570,7 +738,7 @@ class MidiConvert(MusicSequence): ) ) result.append( - SingleCommand( + MineCommand( self.execute_cmd_head.format( "@a[scores={" + scoreboard_name + "=1..}]" ) @@ -581,7 +749,7 @@ class MidiConvert(MusicSequence): ) ) result.append( - SingleCommand( + MineCommand( self.execute_cmd_head.format( "@a[scores={" + scoreboard_name + "=1..}]" ) @@ -593,7 +761,7 @@ class MidiConvert(MusicSequence): ) result.append( - SingleCommand( + MineCommand( self.execute_cmd_head.format( "@a[scores={" + scoreboard_name + "=1..}]" ) @@ -605,7 +773,7 @@ class MidiConvert(MusicSequence): ) result.append( - SingleCommand( + MineCommand( self.execute_cmd_head.format( "@a[scores={" + scoreboard_name + "=1..}]" ) @@ -642,7 +810,7 @@ class MidiConvert(MusicSequence): ) ) result.append( - SingleCommand( + MineCommand( self.execute_cmd_head.format( r"@a[scores={" + scoreboard_name @@ -658,20 +826,20 @@ class MidiConvert(MusicSequence): if r"%%%" in pgs_style: result.append( - SingleCommand( + MineCommand( "scoreboard objectives remove {}PercT".format(sbn_pc), annotation="移除临时百分比变量", ) ) if r"%%t" in pgs_style: result.append( - SingleCommand( + MineCommand( "scoreboard objectives remove {}TMinT".format(sbn_pc), annotation="移除临时分变量", ) ) result.append( - SingleCommand( + MineCommand( "scoreboard objectives remove {}TSecT".format(sbn_pc), annotation="移除临时秒变量", ) @@ -683,9 +851,7 @@ class MidiConvert(MusicSequence): def to_command_list_in_score( self, scoreboard_name: str = "mscplay", - max_volume: float = 1.0, - speed: float = 1.0, - ) -> Tuple[List[List[SingleCommand]], int, int]: + ) -> Tuple[List[List[MineCommand]], int, int]: """ 将midi转换为我的世界命令列表 @@ -693,20 +859,12 @@ class MidiConvert(MusicSequence): ---------- scoreboard_name: str 我的世界的计分板名称 - max_volume: float - 最大播放音量,注意:这里的音量范围为(0,1],如果超出将被处理为正确值,其原理为在距离玩家 (1 / volume -1) 的地方播放 - speed: float - 速度,注意:这里的速度指的是播放倍率,其原理为在播放音频的时候,每个音符的播放时间除以 speed Returns ------- tuple( list[list[SingleCommand指令,... ],... ], int指令数量, int音乐时长游戏刻 ) """ - if speed == 0: - raise ZeroSpeedError("播放速度仅可为(0,1]范围内的正实数") - max_volume = 1 if max_volume > 1 else (0.001 if max_volume <= 0 else max_volume) - command_channels = [] command_amount = 0 max_score = 0 @@ -720,59 +878,44 @@ class MidiConvert(MusicSequence): this_channel = [] for note in channel: - score_now = round(note.start_time / float(speed) / 50) - max_score = max(max_score, score_now) + max_score = max(max_score, note.start_tick) ( mc_sound_ID, - mc_distance_volume, + relative_coordinates, volume_percentage, mc_pitch, - ) = note_to_command_parameters( + ) = minenote_to_command_paramaters( note, - ( - self.percussion_note_referrence_table - if note.percussive - else self.pitched_note_reference_table - ), - deviation=self.music_deviation, - volume_percentage=( - (max_volume) if note.track_no == 0 else (max_volume * 0.9) - ), - volume_processing_method=self.volume_processing_function, + pitch_deviation=self.music_deviation, ) this_channel.append( - SingleCommand( + MineCommand( ( self.execute_cmd_head.format( - "@a[scores=({}={})]".format(scoreboard_name, score_now) + "@a[scores=({}={})]".format( + scoreboard_name, note.start_tick + ) .replace("(", r"{") .replace(")", r"}") ) - + ( - r"playsound {} @s ^ ^ ^{} {}".format( - mc_sound_ID, mc_distance_volume, volume_percentage - ) - if note.percussive - else r"playsound {} @s ^ ^ ^{} {} {}".format( - mc_sound_ID, - mc_distance_volume, - volume_percentage, - mc_pitch, - ) + + r"playsound {} @s ^{} ^{} ^{} {} {} {}".format( + mc_sound_ID, + *relative_coordinates, + volume_percentage, + 1.0 if note.percussive else mc_pitch, + self.minium_volume, ) ), annotation=( - "在{}播放{}%的{}噪音".format( - mctick2timestr(score_now), - max_volume * 100, + "在{}播放噪音{}".format( + mctick2timestr(note.start_tick), mc_sound_ID, ) if note.percussive - else "在{}播放{}%的{}乐音".format( - mctick2timestr(score_now), - max_volume * 100, + else "在{}播放乐音{}".format( + mctick2timestr(note.start_tick), "{}:{:.2f}".format(mc_sound_ID, mc_pitch), ) ), @@ -785,24 +928,17 @@ class MidiConvert(MusicSequence): self.music_command_list.extend(this_channel) command_channels.append(this_channel) - self.music_tick_num = max_score return command_channels, command_amount, max_score def to_command_list_in_delay( self, - max_volume: float = 1.0, - speed: float = 1.0, player_selector: str = "@a", - ) -> Tuple[List[SingleCommand], int, int]: + ) -> Tuple[List[MineCommand], int, int]: """ 将midi转换为我的世界命令列表,并输出每个音符之后的延迟 Parameters ---------- - max_volume: float - 最大播放音量,注意:这里的音量范围为(0,1],如果超出将被处理为正确值,其原理为在距离玩家 (1 / volume -1) 的地方播放音频 - speed: float - 速度,注意:这里的速度指的是播放倍率,其原理为在播放音频的时候,每个音符的播放时间除以 speed player_selector: str 玩家选择器,默认为`@a` @@ -811,25 +947,18 @@ class MidiConvert(MusicSequence): tuple( list[SingleCommand,...], int音乐时长游戏刻, int最大同时播放的指令数量 ) """ - if speed == 0: - raise ZeroSpeedError("播放速度仅可为(0,1]范围内的正实数") - max_volume = 1 if max_volume > 1 else (0.001 if max_volume <= 0 else max_volume) - - notes_list: List[SingleNote] = [] + notes_list: List[MineNote] = sorted( + [i for j in self.channels.values() for i in j], + key=lambda note: note.start_tick, + ) # 此处 我们把通道视为音轨 - for channel in self.channels.values(): - notes_list.extend(channel) - - notes_list.sort(key=lambda a: a.start_time) - self.music_command_list = [] multi = max_multi = 0 delaytime_previous = 0 for note in notes_list: - delaytime_now = round(note.start_time / speed / 50) - if (tickdelay := (delaytime_now - delaytime_previous)) == 0: + if (tickdelay := (note.start_tick - delaytime_previous)) == 0: multi += 1 else: max_multi = max(max_multi, multi) @@ -837,68 +966,53 @@ class MidiConvert(MusicSequence): ( mc_sound_ID, - mc_distance_volume, + relative_coordinates, volume_percentage, mc_pitch, - ) = note_to_command_parameters( + ) = minenote_to_command_paramaters( note, - ( - self.percussion_note_referrence_table - if note.percussive - else self.pitched_note_reference_table - ), - deviation=self.music_deviation, - volume_percentage=( - (max_volume) if note.track_no == 0 else (max_volume * 0.9) - ), - volume_processing_method=self.volume_processing_function, + pitch_deviation=self.music_deviation, ) self.music_command_list.append( - SingleCommand( + MineCommand( command=( self.execute_cmd_head.format(player_selector) - + ( - r"playsound {} @s ^ ^ ^{} {}".format( - mc_sound_ID, mc_distance_volume, volume_percentage - ) - if note.percussive - else r"playsound {} @s ^ ^ ^{} {} {}".format( - mc_sound_ID, - mc_distance_volume, - volume_percentage, - mc_pitch, - ) + + r"playsound {} @s ^{} ^{} ^{} {} {} {}".format( + mc_sound_ID, + *relative_coordinates, + volume_percentage, + 1.0 if note.percussive else mc_pitch, + self.minium_volume, ) ), annotation=( - "在{}播放{}%的{}噪音".format( - mctick2timestr(delaytime_now), - max_volume * 100, + "在{}播放噪音{}".format( + mctick2timestr(note.start_tick), mc_sound_ID, ) if note.percussive - else "在{}播放{}%的{}乐音".format( - mctick2timestr(delaytime_now), - max_volume * 100, + else "在{}播放乐音{}".format( + mctick2timestr(note.start_tick), "{}:{:.2f}".format(mc_sound_ID, mc_pitch), ) ), tick_delay=tickdelay, ), ) - delaytime_previous = delaytime_now + delaytime_previous = note.start_tick - self.music_tick_num = round(notes_list[-1].start_time / speed / 50) - return self.music_command_list, self.music_tick_num, max_multi + 1 + return self.music_command_list, notes_list[-1].start_tick, max_multi + 1 def copy_important(self): - dst = MidiConvert( + dst = MidiConvert.from_mido_obj( midi_obj=mido.MidiFile(), midi_name=self.music_name, enable_old_exe_format=self.enable_old_exe_format, + pitched_note_rtable={}, + percussion_note_rtable={}, + vol_processing_function=lambda a: a, ) 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] - dst.music_tick_num = self.music_tick_num return dst diff --git a/Musicreater/plugin/addonpack/main.py b/Musicreater/plugin/addonpack/main.py index f4655b6..5837e5c 100644 --- a/Musicreater/plugin/addonpack/main.py +++ b/Musicreater/plugin/addonpack/main.py @@ -56,7 +56,7 @@ def to_addon_pack_in_score( """ cmdlist, maxlen, maxscore = midi_cvt.to_command_list_in_score( - scoreboard_name, data_cfg.volume_ratio, data_cfg.speed_multiplier + scoreboard_name=scoreboard_name, ) # 当文件f夹{self.outputPath}/temp/functions存在时清空其下所有项目,然后创建 @@ -180,9 +180,7 @@ def to_addon_pack_in_delay( ) command_list, max_delay = midi_cvt.to_command_list_in_delay( - data_cfg.volume_ratio, - data_cfg.speed_multiplier, - player, + player_selector=player, )[:2] if not os.path.exists(data_cfg.dist_path): @@ -377,9 +375,7 @@ def to_addon_pack_in_repeater( ) command_list, max_delay, max_together = midi_cvt.to_command_list_in_delay( - data_cfg.volume_ratio, - data_cfg.speed_multiplier, - player, + player_selector=player, ) if not os.path.exists(data_cfg.dist_path): diff --git a/Musicreater/plugin/bdx.py b/Musicreater/plugin/bdx.py index 8677164..4f27c0c 100644 --- a/Musicreater/plugin/bdx.py +++ b/Musicreater/plugin/bdx.py @@ -18,7 +18,7 @@ Terms & Conditions: License.md in the root directory from typing import List from ..constants import x, y, z -from ..subclass import SingleCommand +from ..subclass import MineCommand from .common import bottem_side_length_of_smallest_square_bottom_box BDX_MOVE_KEY = { @@ -123,7 +123,7 @@ def form_command_block_in_BDX_bytes( def commands_to_BDX_bytes( - commands_list: List[SingleCommand], + commands_list: List[MineCommand], max_height: int = 64, ): """ diff --git a/Musicreater/plugin/bdxfile/main.py b/Musicreater/plugin/bdxfile/main.py index 51c2e72..07f6bec 100644 --- a/Musicreater/plugin/bdxfile/main.py +++ b/Musicreater/plugin/bdxfile/main.py @@ -17,7 +17,7 @@ import os import brotli from ...main import MidiConvert -from ...subclass import SingleCommand +from ...subclass import MineCommand from ..bdx import ( bdx_move, commands_to_BDX_bytes, @@ -61,16 +61,14 @@ def to_BDX_file_in_score( """ cmdlist, command_count, max_score = midi_cvt.to_command_list_in_score( - scoreboard_name, data_cfg.volume_ratio, data_cfg.speed_multiplier + scoreboard_name=scoreboard_name, ) if not os.path.exists(data_cfg.dist_path): os.makedirs(data_cfg.dist_path) with open( - os.path.abspath( - os.path.join(data_cfg.dist_path, f"{midi_cvt.music_name}.bdx") - ), + os.path.abspath(os.path.join(data_cfg.dist_path, f"{midi_cvt.music_name}.bdx")), "w+", ) as f: f.write("BD@") @@ -83,7 +81,7 @@ def to_BDX_file_in_score( midi_cvt.music_command_list + ( [ - SingleCommand( + MineCommand( command="scoreboard players reset @a[scores={" + scoreboard_name + "=" @@ -118,9 +116,7 @@ def to_BDX_file_in_score( _bytes += cmdBytes with open( - os.path.abspath( - os.path.join(data_cfg.dist_path, f"{midi_cvt.music_name}.bdx") - ), + os.path.abspath(os.path.join(data_cfg.dist_path, f"{midi_cvt.music_name}.bdx")), "ab+", ) as f: f.write(brotli.compress(_bytes + b"XE")) @@ -157,18 +153,14 @@ def to_BDX_file_in_delay( """ cmdlist, max_delay = midi_cvt.to_command_list_in_delay( - data_cfg.volume_ratio, - data_cfg.speed_multiplier, - player, + player_selector=player, )[:2] if not os.path.exists(data_cfg.dist_path): os.makedirs(data_cfg.dist_path) with open( - os.path.abspath( - os.path.join(data_cfg.dist_path, f"{midi_cvt.music_name}.bdx") - ), + os.path.abspath(os.path.join(data_cfg.dist_path, f"{midi_cvt.music_name}.bdx")), "w+", ) as f: f.write("BD@") @@ -216,9 +208,7 @@ def to_BDX_file_in_delay( _bytes += cmdBytes with open( - os.path.abspath( - os.path.join(data_cfg.dist_path, f"{midi_cvt.music_name}.bdx") - ), + os.path.abspath(os.path.join(data_cfg.dist_path, f"{midi_cvt.music_name}.bdx")), "ab+", ) as f: f.write(brotli.compress(_bytes + b"XE")) diff --git a/Musicreater/plugin/main.py b/Musicreater/plugin/main.py index 83d6f94..154af62 100644 --- a/Musicreater/plugin/main.py +++ b/Musicreater/plugin/main.py @@ -23,17 +23,11 @@ from ..subclass import DEFAULT_PROGRESSBAR_STYLE, ProgressBarStyle @dataclass(init=False) -class ConvertConfig: +class ConvertConfig: # 必定要改 """ 转换通用设置存储类 """ - volume_ratio: float - """音量比例""" - - speed_multiplier: float - """速度倍率""" - progressbar_style: Union[ProgressBarStyle, None] """进度条样式""" @@ -43,8 +37,6 @@ class ConvertConfig: def __init__( self, output_path: str, - volume: float = 1.0, - speed: float = 1.0, progressbar: Union[bool, Tuple[str, Tuple[str, str]], ProgressBarStyle] = True, ignore_progressbar_param_error: bool = False, ): @@ -67,12 +59,6 @@ class ConvertConfig: self.dist_path = output_path """输出目录""" - self.volume_ratio = volume - """音量比例""" - - self.speed_multiplier = speed - """速度倍率""" - if progressbar: # 此处是对于仅有 True 的参数和自定义参数的判断 # 改这一段没🐎 diff --git a/Musicreater/plugin/mcstructfile/__init__.py b/Musicreater/plugin/mcstructfile/__init__.py index cb43943..4181be5 100644 --- a/Musicreater/plugin/mcstructfile/__init__.py +++ b/Musicreater/plugin/mcstructfile/__init__.py @@ -21,4 +21,8 @@ __all__ = [ ] __author__ = (("金羿", "Eilles Wan"),) -from .main import to_mcstructure_file_in_delay, to_mcstructure_file_in_repeater, to_mcstructure_file_in_score +from .main import ( + to_mcstructure_file_in_delay, + to_mcstructure_file_in_repeater, + to_mcstructure_file_in_score, +) diff --git a/Musicreater/plugin/mcstructfile/main.py b/Musicreater/plugin/mcstructfile/main.py index ad10ffb..37ce215 100644 --- a/Musicreater/plugin/mcstructfile/main.py +++ b/Musicreater/plugin/mcstructfile/main.py @@ -17,7 +17,7 @@ from typing import Literal # from ...exceptions import CommandFormatError from ...main import MidiConvert from ..main import ConvertConfig -from ...subclass import SingleCommand +from ...subclass import MineCommand from ..mcstructure import ( COMPABILITY_VERSION_117, COMPABILITY_VERSION_119, @@ -58,9 +58,7 @@ def to_mcstructure_file_in_delay( ) cmd_list, max_delay = midi_cvt.to_command_list_in_delay( - data_cfg.volume_ratio, - data_cfg.speed_multiplier, - player, + player_selector=player, )[:2] if not os.path.exists(data_cfg.dist_path): @@ -116,18 +114,17 @@ def to_mcstructure_file_in_score( ) cmd_list, cmd_count, max_delay = midi_cvt.to_command_list_in_score( - scoreboard_name, - data_cfg.volume_ratio, - data_cfg.speed_multiplier, + scoreboard_name=scoreboard_name, ) if not os.path.exists(data_cfg.dist_path): os.makedirs(data_cfg.dist_path) struct, size, end_pos = commands_to_structure( - midi_cvt.music_command_list+( + midi_cvt.music_command_list + + ( [ - SingleCommand( + MineCommand( command="scoreboard players reset @a[scores={" + scoreboard_name + "=" @@ -139,7 +136,9 @@ def to_mcstructure_file_in_score( ] if auto_reset else [] - ), max_height - 1, compability_version_=compability_ver + ), + max_height - 1, + compability_version_=compability_ver, ) with open( @@ -188,9 +187,7 @@ def to_mcstructure_file_in_repeater( ) cmd_list, max_delay, max_multiple_cmd = midi_cvt.to_command_list_in_delay( - data_cfg.volume_ratio, - data_cfg.speed_multiplier, - player, + player_selector=player, ) if not os.path.exists(data_cfg.dist_path): diff --git a/Musicreater/plugin/mcstructure.py b/Musicreater/plugin/mcstructure.py index 99b5985..9dfa033 100644 --- a/Musicreater/plugin/mcstructure.py +++ b/Musicreater/plugin/mcstructure.py @@ -21,7 +21,7 @@ from typing import List, Literal, Tuple from TrimMCStruct import Block, Structure, TAG_Byte, TAG_Long from ..constants import x, y, z -from ..subclass import SingleCommand +from ..subclass import MineCommand from .common import bottem_side_length_of_smallest_square_bottom_box @@ -226,7 +226,7 @@ def form_command_block_in_NBT_struct( def commands_to_structure( - commands: List[SingleCommand], + commands: List[MineCommand], max_height: int = 64, compability_version_: int = COMPABILITY_VERSION_119, ): @@ -313,7 +313,7 @@ def commands_to_structure( def commands_to_redstone_delay_structure( - commands: List[SingleCommand], + commands: List[MineCommand], delay_length: int, max_multicmd_length: int, base_block: str = "concrete", diff --git a/Musicreater/plugin/noteblock.py b/Musicreater/plugin/noteblock.py index 9465425..5900d37 100644 --- a/Musicreater/plugin/noteblock.py +++ b/Musicreater/plugin/noteblock.py @@ -18,7 +18,7 @@ Terms & Conditions: License.md in the root directory from ..exceptions import NotDefineProgramError, ZeroSpeedError from ..main import MidiConvert -from ..subclass import SingleCommand +from ..subclass import MineCommand from ..utils import inst_to_sould_with_deviation, perc_inst_to_soundID_withX # 你以为写完了吗?其实并没有 diff --git a/Musicreater/plugin/schematic.py b/Musicreater/plugin/schematic.py index 3138e22..54091cf 100644 --- a/Musicreater/plugin/schematic.py +++ b/Musicreater/plugin/schematic.py @@ -15,4 +15,4 @@ Terms & Conditions: License.md in the root directory # Email TriM-Organization@hotmail.com # 若需转载或借鉴 许可声明请查看仓库目录下的 License.md -import nbtschematic +# import nbtschematic diff --git a/Musicreater/plugin/websocket/main.py b/Musicreater/plugin/websocket/main.py index 0e7e986..5ea71ca 100644 --- a/Musicreater/plugin/websocket/main.py +++ b/Musicreater/plugin/websocket/main.py @@ -13,17 +13,17 @@ Terms & Conditions: License.md in the root directory import fcwslib -from ...main import MidiConvert +# from ...main import MidiConvert -from ..main import ConvertConfig -from ...subclass import SingleCommand +# from ..main import ConvertConfig +# from ...subclass import MineCommand -def open_websocket_server( - midi_cvt: MidiConvert, - data_cfg: ConvertConfig, - player: str = "@a", - server_dist: str = "localhost", - server_port: int = 8000, -): - wssever = fcwslib.Server(server=server_dist,port=server_port,debug_mode=False) \ No newline at end of file +# def open_websocket_server( +# midi_cvt: MidiConvert, +# data_cfg: ConvertConfig, +# player: str = "@a", +# server_dist: str = "localhost", +# server_port: int = 8000, +# ): +# wssever = fcwslib.Server(server=server_dist,port=server_port,debug_mode=False) \ No newline at end of file diff --git a/Musicreater/subclass.py b/Musicreater/subclass.py index bdd3d59..2baa81f 100644 --- a/Musicreater/subclass.py +++ b/Musicreater/subclass.py @@ -18,12 +18,207 @@ Terms & Conditions: License.md in the root directory from dataclasses import dataclass -from typing import Any -from .types import Optional, Any, List, Mapping +from .types import Optional, Any, List, Mapping, Tuple, Union from .constants import MC_PERCUSSION_INSTRUMENT_LIST +@dataclass(init=False) +class MineNote: + """存储单个音符的类""" + + sound_name: str + """乐器ID""" + + note_pitch: int + """midi音高""" + + velocity: int + """响度(力度)""" + + start_tick: int + """开始之时 命令刻""" + + duration: int + """音符持续时间 命令刻""" + + track_no: int + """音符所处的音轨""" + + percussive: bool + """是否作为打击乐器启用""" + + position_displacement: Tuple[float, float, float] + """声像位移""" + + extra_info: Any + """你觉得放什么好?""" + + def __init__( + self, + mc_sound_name: str, + midi_pitch: Optional[int], + midi_velocity: int, + start_time: int, + last_time: int, + track_number: int = 0, + is_percussion: Optional[bool] = None, + displacement: Optional[Tuple[float, float, float]] = None, + extra_information: Optional[Any] = None, + ): + """用于存储单个音符的类 + :param mc_sound_name:`str` 《我的世界》声音ID + :param midi_pitch:`int` midi音高 + :param midi_velocity:`int` midi响度(力度) + :param start_time:`int` 开始之时(命令刻) + 注:此处的时间是用从乐曲开始到当前的毫秒数 + :param last_time:`int` 音符延续时间(命令刻) + :param track_number:`int` 音轨编号 + :param is_percussion:`bool` 是否作为打击乐器 + :param displacement:`tuple[int,int,int]` 声像位移 + :param extra_information:`Any` 附加信息""" + self.sound_name: str = mc_sound_name + """乐器ID""" + self.note_pitch: int = 66 if midi_pitch is None else midi_pitch + """midi音高""" + self.velocity: int = midi_velocity + """响度(力度)""" + self.start_tick: int = start_time + """开始之时 tick""" + self.duration: int = last_time + """音符持续时间 tick""" + self.track_no: int = track_number + """音符所处的音轨""" + + self.percussive = ( + (mc_sound_name in MC_PERCUSSION_INSTRUMENT_LIST) + if (is_percussion is None) + else is_percussion + ) + """是否为打击乐器""" + + self.position_displacement = ( + (0, 0, 0) if (displacement is None) else displacement + ) + """声像位移""" + + 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) + + def set_info(self, sth: Any): + """设置附加信息""" + self.extra_info = sth + + def __str__(self, is_displacement: bool = False, is_track: bool = False): + return "{}Note(Instrument = {}, {}Velocity = {}, StartTick = {}, Duration = {}{}{})".format( + "Percussive" if self.percussive else "", + self.sound_name, + "" if self.percussive else "NotePitch = {}, ".format(self.note_pitch), + self.start_tick, + self.duration, + ", Track = {}".format(self.track_no) if is_track else "", + ( + ", PositionDisplacement = {}".format(self.position_displacement) + if is_displacement + else "" + ), + ) + + def tuplize(self, is_displacement: bool = False, is_track: bool = False): + tuplized = self.__tuple__() + return ( + tuplized[:-2] + + ((tuplized[-2],) if is_track else ()) + + ((tuplized[-1],) if is_displacement else ()) + ) + + def __list__(self) -> List: + return ( + [ + self.percussive, + self.sound_name, + self.velocity, + self.start_tick, + self.duration, + self.track_no, + self.position_displacement, + ] + if self.percussive + else [ + self.percussive, + self.sound_name, + self.note_pitch, + self.velocity, + self.start_tick, + self.duration, + self.track_no, + self.position_displacement, + ] + ) + + def __tuple__( + self, + ) -> Union[ + Tuple[bool, str, int, int, int, int, int, Tuple[float, float, float]], + Tuple[bool, str, int, int, int, int, Tuple[float, float, float]], + ]: + return ( + ( + self.percussive, + self.sound_name, + self.velocity, + self.start_tick, + self.duration, + self.track_no, + self.position_displacement, + ) + if self.percussive + else ( + self.percussive, + self.sound_name, + self.note_pitch, + self.velocity, + self.start_tick, + self.duration, + self.track_no, + self.position_displacement, + ) + ) + + def __dict__(self): + return ( + { + "Percussive": self.percussive, + "Instrument": self.sound_name, + "Velocity": self.velocity, + "StartTick": self.start_tick, + "Duration": self.duration, + "Track": self.track_no, + "PositionDisplacement": self.position_displacement, + } + if self.percussive + else { + "Percussive": self.percussive, + "Instrument": self.sound_name, + "Pitch": self.note_pitch, + "Velocity": self.velocity, + "StartTick": self.start_tick, + "Duration": self.duration, + "Track": self.track_no, + "PositionDisplacement": self.position_displacement, + } + ) + + def __eq__(self, other) -> bool: + if not isinstance(other, self.__class__): + return False + return self.tuplize() == other.tuplize() + + @dataclass(init=False) class SingleNote: """存储单个音符的类""" @@ -59,8 +254,8 @@ class SingleNote: velocity: int, startime: int, lastime: int, + is_percussion: bool, track_number: int = 0, - is_percussion: Optional[bool] = None, extra_information: Any = None, ): """用于存储单个音符的类 @@ -82,12 +277,7 @@ class SingleNote: """音符持续时间 ms""" self.track_no: int = track_number """音符所处的音轨""" - - self.percussive = ( - (is_percussion in MC_PERCUSSION_INSTRUMENT_LIST) - if (is_percussion is None) - else is_percussion - ) + self.percussive: bool = is_percussion """是否为打击乐器""" self.extra_info = extra_information @@ -176,7 +366,7 @@ class SingleNote: @dataclass(init=False) -class SingleCommand: +class MineCommand: """存储单个指令的类""" command_text: str @@ -218,7 +408,7 @@ class SingleCommand: self.annotation_text = annotation def copy(self): - return SingleCommand( + return MineCommand( command=self.command_text, condition=self.conditional, tick_delay=self.delay, @@ -399,3 +589,14 @@ NoteChannelType = Mapping[ Dict[int,Dict[int,List[SingleNote,],],] """ + + +MineNoteChannelType = Mapping[ + int, + List[MineNote,], +] +""" +我的世界频道信息类型 + +Dict[int,Dict[int,List[MineNote,],],] +""" diff --git a/Musicreater/utils.py b/Musicreater/utils.py index 7deacf3..053697c 100644 --- a/Musicreater/utils.py +++ b/Musicreater/utils.py @@ -18,8 +18,8 @@ Terms & Conditions: License.md in the root directory import math import random -from .constants import MM_INSTRUMENT_RANGE_TABLE, MC_INSTRUMENT_BLOCKS_TABLE -from .subclass import SingleNote +from .constants import MC_INSTRUMENT_BLOCKS_TABLE, MM_INSTRUMENT_RANGE_TABLE +from .subclass import SingleNote, MineNote from .types import ( Any, @@ -74,23 +74,25 @@ def inst_to_sould_with_deviation( ------- tuple(str我的世界乐器名, int转换算法中的X) """ - return reference_table.get( - instrumentID, - default=default_instrument, - ), 6 + return ( + reference_table.get( + instrumentID, + default_instrument, + ), + 6, + ) # 明明已经走了 # 凭什么还要在我心里留下缠绵缱绻 -def midi_inst_to_mc_sould( +def midi_inst_to_mc_sound( instrumentID: int, reference_table: MidiInstrumentTableType, default_instrument: str = "note.flute", ) -> str: """ - 返回midi的乐器ID对应的我的世界乐器名,对于音域转换算法,如下: - 2**( ( msg.note - 66 ) / 12 ) 即为MC的音高 + 返回midi的乐器ID对应的我的世界乐器名 Parameters ---------- @@ -101,17 +103,13 @@ def midi_inst_to_mc_sould( Returns ------- - tuple(str我的世界乐器名, int转换算法中的X) + str我的世界乐器名 """ return reference_table.get( instrumentID, - default=default_instrument, + default_instrument, ) - # 明明已经走了 - # 凭什么还要在我心里留下缠绵缱绻 - - def natural_curve( vol: float, @@ -157,44 +155,167 @@ def straight_line(vol: float) -> float: return vol / -8 + 16 -def note_to_command_parameters( - note_: SingleNote, - reference_table: MidiInstrumentTableType, - deviation: int = 0, - volume_percentage: float = 1, - volume_processing_method: Callable[[float], float] = natural_curve, +def minenote_to_command_paramaters( + note_: MineNote, + pitch_deviation: float = 0, ) -> Tuple[ str, - float, + Tuple[float, float, float], float, Union[float, Literal[None]], ]: """ - 将音符转为播放的指令 + 将MineNote对象转为《我的世界》音符播放所需之参数 + :param note_:MineNote 音符对象 + :param deviation:float 音调偏移量 + + :return str[我的世界音符ID], Tuple[float,float,float]播放视角坐标, float[指令音量参数], float[指令音调参数] + """ + + return ( + note_.sound_name, + note_.position_displacement, + note_.velocity / 127, + ( + None + if note_.percussive + else 2 ** ((note_.note_pitch - 66 + pitch_deviation) / 12) + ), + ) + + +def single_note_to_command_parameters( + note_: SingleNote, + reference_table: MidiInstrumentTableType, + deviation: float = 0, + volume_processing_method: Callable[[float], float] = natural_curve, +) -> Tuple[ + str, + Tuple[float, float, float], + float, + Union[float, Literal[None]], +]: + """ + 将音符转为播放的指令之参数 :param note_:int 音符对象 :param reference_table:Dict[int, str] 转换对照表 - :param deviation:int 音调偏移量 - :param volume_percentage:int 音量占比(0,1] + :param deviation:float 音调偏移量 :param volume_proccessing_method:Callable[[float], float] 音量处理函数 - :return str[我的世界音符ID], float[播放距离], float[指令音量参数], float[指令音调参数] + :return str[我的世界音符ID], Tuple[float,float,float]播放视角坐标, float[指令音量参数], float[指令音调参数] """ - mc_sound_ID = midi_inst_to_mc_sould( + + mc_sound_ID = midi_inst_to_mc_sound( note_.inst, reference_table, "note.bd" if note_.percussive else "note.flute", ) - # delaytime_now = round(self.start_time / float(speed) / 50) - mc_pitch = None if note_.percussive else 2 ** ((note_.note - 66 + deviation) / 12) + mc_distance_volume = volume_processing_method(note_.velocity) - mc_distance_volume = volume_processing_method(note_.velocity * volume_percentage) - - return mc_sound_ID, mc_distance_volume, volume_percentage, mc_pitch + return ( + mc_sound_ID, + (0, mc_distance_volume, 0), + note_.velocity / 127, + None if note_.percussive else 2 ** ((note_.pitch - 66 + deviation) / 12), + ) -def from_single_note( - note_: SingleNote, random_select: bool = False, default_block: str = "air" +def midi_msgs_to_minenote( + inst_: int, # 乐器编号 + note_: int, + percussive_: bool, # 是否作为打击乐器启用 + velocity_: int, + start_time_: int, + duration_: int, + track_no_: int, + play_speed: float, + midi_reference_table: MidiInstrumentTableType, + volume_processing_method_: Callable[[float], float], +) -> MineNote: + """ + 将Midi信息转为我的世界音符对象 + :param inst_: int 乐器编号 + :param note_: int 音高编号(音符编号) + :param percussive_: bool 是否作为打击乐器启用 + :param velocity_: int 力度(响度) + :param start_time_: int 音符起始时间(毫秒数) + :param duration_: int 音符持续时间(毫秒数) + :param track_no_: int 音符所处音轨 + :param play_speed: float 曲目播放速度 + :param midi_reference_table: Dict[int, str] 转换对照表 + :param volume_proccessing_method_: Callable[[float], float] 音量处理函数 + + :return MineNote我的世界音符对象 + """ + mc_sound_ID = midi_inst_to_mc_sound( + inst_, + midi_reference_table, + "note.bd" if percussive_ else "note.flute", + ) + + 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), + ) + + +def single_note_to_minenote( + note_: SingleNote, + reference_table: MidiInstrumentTableType, + play_speed: float = 0, + volume_processing_method: Callable[[float], float] = natural_curve, +) -> MineNote: + """ + 将音符转为我的世界音符对象 + :param note_:SingleNote 音符对象 + :param reference_table:Dict[int, str] 转换对照表 + :param play_speed:float 播放速度 + :param volume_proccessing_method:Callable[[float], float] 音量处理函数 + + :return MineNote我的世界音符对象 + """ + mc_sound_ID = midi_inst_to_mc_sound( + note_.inst, + reference_table, + "note.bd" if note_.percussive else "note.flute", + ) + + mc_distance_volume = volume_processing_method(note_.velocity) + + return MineNote( + mc_sound_ID, + note_.pitch, + note_.velocity, + round(note_.start_time / float(play_speed) / 50), + round(note_.duration / float(play_speed) / 50), + note_.track_no, + note_.percussive, + (0, mc_distance_volume, 0), + note_.extra_info, + ) + + +def is_in_diapason(note_pitch: int, instrument: str) -> bool: + note_range = MM_INSTRUMENT_RANGE_TABLE.get(instrument, ((-1, 128), 0))[0] + return note_pitch >= note_range[0] and note_pitch <= note_range[1] + + +def is_note_in_diapason(note_: MineNote) -> bool: + note_range = MM_INSTRUMENT_RANGE_TABLE.get(note_.sound_name, ((-1, 128), 0))[0] + return note_.note_pitch >= note_range[0] and note_.note_pitch <= note_range[1] + + +def note_to_redstone_block( + note_: MineNote, random_select: bool = False, default_block: str = "air" ): """ 将我的世界乐器名改作音符盒所需的对应方块名称 diff --git a/README.md b/README.md index 0d3b7da..5e3ff2d 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@
-
@@ -33,7 +33,7 @@
## 介绍 🚀
-音·创 是一个免费开源的针对 **《我的世界》** 的 MIDI 音乐转换库
+音·创 是一款免费开源的针对 **《我的世界》** 音乐的支持库
欢迎加群:[861684859](https://jq.qq.com/?_wv=1027&k=hpeRxrYr)
@@ -120,7 +120,7 @@ NOT APPROVED BY OR ASSOCIATED WITH MOJANG OR MICROSOFT.
NOT APPROVED BY OR ASSOCIATED WITH NETEASE.
-[Bilibili: 金羿ELS]: https://img.shields.io/badge/Bilibili-%E5%87%8C%E4%BA%91%E9%87%91%E7%BE%BF-00A1E7?style=for-the-badge
+[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
diff --git a/README_EN.md b/README_EN.md
index 4538424..f620cf3 100644
--- a/README_EN.md
+++ b/README_EN.md
@@ -32,7 +32,7 @@
## Introduction🚀
-Musicreater is a free open-source library used for converting digital music files into formats that could be read in _Minecraft_.
+Musicreater is a free open-source library used for digital music that being played in _Minecraft_.
Welcome to join our QQ group: [861684859](https://jq.qq.com/?_wv=1027&k=hpeRxrYr)
@@ -121,7 +121,7 @@ NOT APPROVED BY OR ASSOCIATED WITH NETEASE.
- 上文提及的 网易 公司,指代的是在中国大陆运营《我的世界:中国版》的上海网之易网络科技发展有限公司
-[Bilibili: Eilles]: https://img.shields.io/badge/Bilibili-%E5%87%8C%E4%BA%91%E9%87%91%E7%BE%BF-00A1E7?style=for-the-badge
+[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
diff --git a/RSMC_test.py b/RSMC_test.py
deleted file mode 100644
index 08c1ea4..0000000
--- a/RSMC_test.py
+++ /dev/null
@@ -1,8 +0,0 @@
-import Musicreater.experiment
-
-
-print(
- Musicreater.experiment.FutureMidiConvertRSNB.from_midi_file(
- input("midi路径:"), old_exe_format=False
- ).to_note_list_in_delay()
-)
diff --git a/example.py b/example.py
index f3c3cfc..fbc9a22 100644
--- a/example.py
+++ b/example.py
@@ -80,11 +80,11 @@ else:
# 提示语 检测函数 错误提示语
for args in [
(
- f"输入音量:",
+ f"最小播放音量:",
float,
),
(
- f"输入播放速度:",
+ f"播放速度:",
float,
),
(
@@ -118,21 +118,19 @@ else:
if fileFormat == 1
else (
(
- (
- "结构延展方向:",
- lambda a: isin(
- a,
- {
- "z+": ["z+", "Z+"],
- "x+": ["X+", "x+"],
- "z-": ["Z-", "z-"],
- "x-": ["x-", "X-"],
- },
- ),
- )
- if (playerFormat == 2 and fileFormat == 2)
- else ()
- ),
+ "结构延展方向:",
+ lambda a: isin(
+ a,
+ {
+ "z+": ["z+", "Z+"],
+ "x+": ["X+", "x+"],
+ "z-": ["Z-", "z-"],
+ "x-": ["x-", "X-"],
+ },
+ ),
+ )
+ if (playerFormat == 2 and fileFormat == 2)
+ else ()
)
),
(
@@ -152,12 +150,17 @@ else:
),
]:
if args:
- prompts.append(args[1](input(args[0])))
+ try:
+ prompts.append(args[1](input(args[0])))
+ except Exception:
+ print(args)
print(f"正在处理 {midi_path} :")
-cvt_mid = Musicreater.MidiConvert.from_midi_file(midi_path, old_exe_format=False)
-cvt_cfg = ConvertConfig(out_path, *prompts[:3])
+cvt_mid = Musicreater.MidiConvert.from_midi_file(
+ midi_path, old_exe_format=False, min_volume=prompts[0], play_speed=prompts[1]
+)
+cvt_cfg = ConvertConfig(out_path, prompts[2])
if fileFormat == 0:
if playerFormat == 1:
diff --git a/example_futureFunction.py b/example_futureFunction.py
index 0daec0b..460eff0 100644
--- a/example_futureFunction.py
+++ b/example_futureFunction.py
@@ -7,7 +7,7 @@ print(
Musicreater.experiment.FutureMidiConvertM4.from_midi_file(
input("midi路径:"), old_exe_format=False
),
- Musicreater.plugin.ConvertConfig(input("输出路径:"), volume=1),
+ Musicreater.plugin.ConvertConfig(input("输出路径:"),),
max_height=32,
)
)
diff --git a/setup.py b/setup.py
index 3f05cb8..4f64130 100644
--- a/setup.py
+++ b/setup.py
@@ -15,7 +15,7 @@ setuptools.setup(
version=Musicreater.__version__,
author="Eilles Wan, bgArray",
author_email="TriM-Organization@hotmail.com",
- description="一款免费开源的 《我的世界》 mid音乐转换库。\n"
+ description="一款免费开源的针对《我的世界》音乐的支持库\n"
"A free open-source python library used to convert midi into Minecraft.",
long_description=long_description,
long_description_content_type="text/markdown",