diff --git a/Musicreater/__init__.py b/Musicreater/__init__.py index f538afc..3dd9595 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.2.1" -__vername__ = "扩大对有问题的Midi文件的兼容性" +__version__ = "1.4.0" +__vername__ = "红石指令音乐的生成" __author__ = (("金羿", "Eilles Wan"), ("诸葛亮与八卦阵", "bgArray"), ("鸣凤鸽子", "MingFengPigeon")) __all__ = [ # 主要类 diff --git a/Musicreater/constants.py b/Musicreater/constants.py index b89e0ee..0002f89 100644 --- a/Musicreater/constants.py +++ b/Musicreater/constants.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- +from typing import Dict, List, Tuple + """ 存放常量与数值性内容 """ @@ -40,30 +42,34 @@ DEFAULT_PROGRESSBAR_STYLE = ( 默认的进度条样式组 """ -PITCHED_INSTRUMENT_LIST = { + +# 以下是由 Touch “偷吃” 带来的高准确率音效对照表 +# 包括乐音乐器对照和打击乐器对照 + +PITCHED_INSTRUMENT_TABLE: Dict[int, Tuple[str, int]] = { 0: ("note.harp", 6), 1: ("note.harp", 6), 2: ("note.pling", 6), 3: ("note.harp", 6), 4: ("note.pling", 6), 5: ("note.pling", 6), - 6: ("note.harp", 6), + 6: ("note.guitar", 7), 7: ("note.harp", 6), - 8: ("note.share", 7), # 打击乐器无音域 - 9: ("note.harp", 6), - 10: ("note.didgeridoo", 8), - 11: ("note.harp", 6), - 12: ("note.xylophone", 4), - 13: ("note.chime", 4), - 14: ("note.harp", 6), + 8: ("note.bell", 4), # 打击乐器无音域 + 9: ("note.bell", 4), + 10: ("note.iron_xylophone", 6), + 11: ("note.iron_xylophone", 6), + 12: ("note.iron_xylophone", 6), + 13: ("note.xylophone", 4), + 14: ("note.chime", 4), 15: ("note.harp", 6), 16: ("note.bass", 8), 17: ("note.harp", 6), - 18: ("note.harp", 6), + 18: ("note.flute", 5), 19: ("note.harp", 6), 20: ("note.harp", 6), - 21: ("note.harp", 6), - 22: ("note.harp", 6), + 21: ("note.flute", 5), + 22: ("note.flute", 5), 23: ("note.guitar", 7), 24: ("note.guitar", 7), 25: ("note.guitar", 7), @@ -81,21 +87,21 @@ PITCHED_INSTRUMENT_LIST = { 37: ("note.bass", 8), 38: ("note.bass", 8), 39: ("note.bass", 8), - 40: ("note.harp", 6), - 41: ("note.harp", 6), - 42: ("note.harp", 6), - 43: ("note.harp", 6), + 40: ("note.flute", 5), + 41: ("note.flute", 5), + 42: ("note.flute", 5), + 43: ("note.flute", 5), 44: ("note.iron_xylophone", 6), 45: ("note.guitar", 7), 46: ("note.harp", 6), - 47: ("note.harp", 6), + 47: ("note.bd", 7), 48: ("note.guitar", 7), 49: ("note.guitar", 7), 50: ("note.bit", 6), 51: ("note.bit", 6), - 52: ("note.harp", 6), - 53: ("note.harp", 6), - 54: ("note.bit", 6), + 52: ("note.flute", 5), + 53: ("note.flute", 5), + 54: ("note.flute", 5), 55: ("note.flute", 5), 56: ("note.flute", 5), 57: ("note.flute", 5), @@ -110,16 +116,16 @@ PITCHED_INSTRUMENT_LIST = { 66: ("note.bit", 6), 67: ("note.bit", 6), 68: ("note.flute", 5), - 69: ("note.harp", 6), - 70: ("note.harp", 6), + 69: ("note.bit", 6), + 70: ("note.banjo", 6), 71: ("note.flute", 5), 72: ("note.flute", 5), 73: ("note.flute", 5), - 74: ("note.harp", 6), + 74: ("note.flute", 5), 75: ("note.flute", 5), 76: ("note.harp", 6), 77: ("note.harp", 6), - 78: ("note.harp", 6), + 78: ("note.flute", 5), 79: ("note.harp", 6), 80: ("note.bit", 6), 81: ("note.bit", 6), @@ -149,35 +155,35 @@ PITCHED_INSTRUMENT_LIST = { 105: ("note.banjo", 6), 106: ("note.harp", 6), 107: ("note.harp", 6), - 108: ("note.harp", 6), - 109: ("note.harp", 6), - 110: ("note.harp", 6), + 108: ("note.bell", 4), + 109: ("note.flute", 5), + 110: ("note.flute", 5), 111: ("note.guitar", 7), - 112: ("note.harp", 6), + 112: ("note.bell", 4), 113: ("note.bell", 4), - 114: ("note.harp", 6), + 114: ("note.flute", 5), 115: ("note.cow_bell", 5), 116: ("note.bd", 7), # 打击乐器无音域 117: ("note.bass", 8), 118: ("note.bit", 6), - 119: ("note.bd", 7), # 打击乐器无音域 + 119: ("firework.blast", 7), # 打击乐器无音域 120: ("note.guitar", 7), 121: ("note.harp", 6), 122: ("note.harp", 6), 123: ("note.harp", 6), 124: ("note.harp", 6), 125: ("note.hat", 7), # 打击乐器无音域 - 126: ("note.bd", 7), # 打击乐器无音域 + 126: ("firework.twinkle", 7), # 打击乐器无音域 127: ("note.snare", 7), # 打击乐器无音域 } -PERCUSSION_INSTRUMENT_LIST = { +PERCUSSION_INSTRUMENT_TABLE: Dict[int, Tuple[str, int]] = { 34: ("note.bd", 7), 35: ("note.bd", 7), 36: ("note.hat", 7), 37: ("note.snare", 7), 38: ("note.snare", 7), - 39: ("note.snare", 7), + 39: ("fire.ignite", 7), 40: ("note.hat", 7), 41: ("note.snare", 7), 42: ("note.hat", 7), @@ -221,11 +227,22 @@ PERCUSSION_INSTRUMENT_LIST = { 80: ("note.bell", 4), } -INSTRUMENT_BLOCKS_LIST = { +PERCUSSION_INSTRUMENT_LIST: List[str] = [ + "note.snare", + "note.bd", + "note.hat", + "note.basedrum", + "firework.blast", + "firework.twinkle", + "fire.ignite", +] + +INSTRUMENT_BLOCKS_TABLE: Dict[str, Tuple[str]] = { "note.bass": ("planks",), "note.snare": ("sand",), "note.hat": ("glass",), "note.bd": ("stone",), + "note.basedrum": ("stone",), "note.bell": ("gold_block",), "note.flute": ("clay",), "note.chime": ("packed_ice",), @@ -238,7 +255,11 @@ INSTRUMENT_BLOCKS_LIST = { "note.banjo": ("hay_block",), "note.pling": ("glowstone",), "note.bassattack": ("command_block",), # 无法找到此音效 - "note.harp": ("glass",), + "note.harp": ("dirt",), + # 呃…… + "firework.blast": ("sandstone",), + "firework.twinkle": ("red_sandstone",), + "fire.ignite": ("concrete_powder",), } diff --git a/Musicreater/experiment.py b/Musicreater/experiment.py index fa32e6f..fa4ab6b 100644 --- a/Musicreater/experiment.py +++ b/Musicreater/experiment.py @@ -16,14 +16,144 @@ Terms & Conditions: License.md in the root directory # Email TriM-Organization@hotmail.com # 若需转载或借鉴 许可声明请查看仓库目录下的 License.md +import random from typing import Dict, List, Tuple, Union +from .constants import INSTRUMENT_BLOCKS_TABLE from .exceptions import * from .main import MidiConvert from .subclass import * from .utils import * +class FutureMidiConvertRSNB(MidiConvert): + """ + 加入红石音乐适配 + """ + + music_command_list: Dict[int, SingleNoteBox] + """音乐指令列表""" + + @staticmethod + def soundID_to_block(sound_id: str, random_select: bool = False) -> str: + """ + 将我的世界乐器名改作音符盒所需的对应方块名称 + + Parameters + ---------- + sound_id: str + 将我的世界乐器名 + random_select: bool + 是否随机选取对应方块 + + Returns + ------- + str方块名称 + """ + try: + if random_select: + return random.choice(INSTRUMENT_BLOCKS_TABLE[sound_id]) + else: + return INSTRUMENT_BLOCKS_TABLE[sound_id][0] + except KeyError: + return "air" + + def to_note_list_in_delay( + self, + ) -> Tuple[Dict[int, SingleNoteBox], int]: + """ + 使用金羿的转换思路,将midi转换为我的世界音符盒音高、乐器及延迟列表,并输出每个音符之后的延迟 + + Returns + ------- + tuple( Dict[int, SingleNoteBox], int音乐时长游戏刻 ) + """ + + self.to_music_channels() + + tracks = {} + note_range = {} + InstID = -1 + # cmd_amount = 0 + + # 此处 我们把通道视为音轨 + for i in self.channels.keys(): + # 如果当前通道为空 则跳过 + if not self.channels[i]: + continue + + # 第十通道是打击乐通道 + SpecialBits = True if i == 9 else False + + for track_no, track in self.channels[i].items(): + for msg in track: + if msg[0] == "PgmC": + InstID = msg[1] + + elif msg[0] == "NoteS": + # block_id = self.soundID_to_block(( + # self.perc_inst_to_soundID_withX(msg[1]) + # if SpecialBits + # else self.inst_to_souldID_withX(InstID) + # )[0]) + + # delaytime_now = round(msg[-1] / 50) + + note_ = 2 ** ((msg[1] - 60) / 12) + + try: + tracks[msg[-1]].append( + ( + InstID, + note_, + ) + ) + except KeyError: + tracks[msg[-1]] = [(InstID, note_)] + + try: + note_range[InstID]["max"] = max( + note_range[InstID]["max"], note_ + ) + note_range[InstID]["min"] = min( + note_range[InstID]["min"], note_ + ) + except KeyError: + note_range[InstID] = {"max": note_, "min": note_} + + del InstID + all_ticks = list(tracks.keys()) + all_ticks.sort() + results = [] + print(note_range) + + exit() + + for i in range(len(all_ticks)): + for j in range(len(tracks[all_ticks[i]])): + results.append( + SingleCommand( + tracks[all_ticks[i]][j], + tick_delay=( + 0 + if j != 0 + else ( + all_ticks[i] - all_ticks[i - 1] + if i != 0 + else all_ticks[i] + ) + ), + annotation="在{}播放{}%的{}音".format( + mctick2timestr(i), max_volume * 100, "" + ), + ) + ) + + self.music_command_list = results + self.music_tick_num = max(all_ticks) + return results, self.music_tick_num + + class FutureMidiConvertM4(MidiConvert): """ 加入插值算法优化音感 @@ -47,9 +177,7 @@ class FutureMidiConvertM4(MidiConvert): ] # print(totalCount) - result: List[ - Tuple[int, int, int, int, float], - ] = [] + result: List[Tuple[int, int, int, int, float],] = [] for _i in range(totalCount): result.append( @@ -105,7 +233,6 @@ class FutureMidiConvertM4(MidiConvert): # nowChannel = [] for track_no, track in self.channels[i].items(): - noteMsgs = [] MsgIndex = [] @@ -152,7 +279,6 @@ class FutureMidiConvertM4(MidiConvert): for every_note in self._linear_note( note, 50 if note.track_no == 0 else 500 ): - soundID, _X = ( self.perc_inst_to_soundID_withX(note.pitch) if SpecialBits @@ -163,7 +289,13 @@ class FutureMidiConvertM4(MidiConvert): max_score = max(max_score, score_now) mc_pitch = 2 ** ((note.pitch - 60 - _X) / 12) - blockmeter = 1 / (1 if note.track_no == 0 else 0.9) / max_volume / every_note[4] - 1 + blockmeter = ( + 1 + / (1 if note.track_no == 0 else 0.9) + / max_volume + / every_note[4] + - 1 + ) track_now.append( SingleCommand( @@ -237,7 +369,6 @@ class FutureMidiConvertM4(MidiConvert): # nowChannel = [] for track_no, track in self.channels[i].items(): - noteMsgs = [] MsgIndex = [] @@ -282,7 +413,6 @@ class FutureMidiConvertM4(MidiConvert): for every_note in self._linear_note( note, 50 if note.track_no == 0 else 500 ): - soundID, _X = ( self.perc_inst_to_soundID_withX(note.pitch) if SpecialBits diff --git a/Musicreater/magicmain.py b/Musicreater/magicmain.py index fa657d8..b14060c 100644 --- a/Musicreater/magicmain.py +++ b/Musicreater/magicmain.py @@ -31,69 +31,6 @@ Terms & Conditions: ../License.md - - -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): - - 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) - ) - maxscore = max(maxscore, nowscore) - soundID, _X = self.__Inst2soundID_withX(instrumentID) - 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] - - - - - # ============================ @@ -209,16 +146,13 @@ if __name__ == '__main__': # ============================ - - -from typing import Union -from .plugin import x,y,z,bottem_side_length_of_smallest_square_bottom_box,form_note_block_in_NBT_struct,form_repeater_in_NBT_struct +from typing import Literal +from ..constants import x,y,z # 不要用 没写完 def delay_to_note_blocks( baseblock: str = "stone", - position_forward: Union(x, y, z) = z, - max_height: int = 64, + position_forward: Literal['x','y','z'] = z, ): """传入音符,生成以音符盒存储的红石音乐 :param: @@ -229,10 +163,6 @@ def delay_to_note_blocks( from TrimMCStruct import Structure, Block - _sideLength = bottem_side_length_of_smallest_square_bottom_box( - len(commands), max_height - ) - struct = Structure( (_sideLength, max_height, _sideLength), # 声明结构大小 ) @@ -292,5 +222,4 @@ def delay_to_note_blocks( struct.set_block(Block("universal_minecraft", baseblock), startpos) startpos[0] += posadder[0] startpos[1] += posadder[1] - startpos[2] += posadder[2] - + startpos[2] += posadder[2] \ No newline at end of file diff --git a/Musicreater/main.py b/Musicreater/main.py index c6dcf98..1468407 100644 --- a/Musicreater/main.py +++ b/Musicreater/main.py @@ -25,7 +25,6 @@ Terms & Conditions: License.md in the root directory # 赶快呼叫 程序员!Let's Go! 直ぐに呼びましょプログラマ レッツゴー! - import math import os from typing import List, Literal, Tuple, Union @@ -183,12 +182,19 @@ class MidiConvert: """文件名,不含路径且不含后缀""" try: - return cls(mido.MidiFile(midi_file_path,clip=True), midi_music_name, old_exe_format) + return cls( + mido.MidiFile(midi_file_path, clip=True), + midi_music_name, + old_exe_format, + ) except (ValueError, TypeError) as E: raise MidiDestroyedError(f"文件{midi_file_path}损坏:{E}") except FileNotFoundError as E: raise FileNotFoundError(f"文件{midi_file_path}不存在:{E}") + # ……真的那么重要吗 + # 我又几曾何时,知道祂真的会抛下我 + @staticmethod def inst_to_souldID_withX( instrumentID: int, @@ -214,7 +220,7 @@ class MidiConvert: tuple(str我的世界乐器名, int转换算法中的X) """ try: - return PITCHED_INSTRUMENT_LIST[instrumentID] + return PITCHED_INSTRUMENT_TABLE[instrumentID] except KeyError: return "note.flute", 5 @@ -233,17 +239,12 @@ class MidiConvert: tuple(str我的世界乐器名, int转换算法中的X) """ try: - return PERCUSSION_INSTRUMENT_LIST[instrumentID] + return PERCUSSION_INSTRUMENT_TABLE[instrumentID] except KeyError: - print("WARN", f"无法使用打击乐器列表库,或者使用了不存在的乐器,打击乐器使用Dislink算法代替。{instrumentID}") - if instrumentID == 55: - return "note.cow_bell", 5 - elif instrumentID in [41, 43, 45]: - return "note.hat", 7 - elif instrumentID in [36, 37, 39]: - return "note.snare", 7 - else: - return "note.bd", 7 + return "note.bd", 7 + + # 明明已经走了 + # 凭什么还要在我心里留下缠绵缱绻 def form_progress_bar( self, @@ -356,6 +357,10 @@ class MidiConvert: ) ) + # 那是假的 + # 一切都并未留下痕迹啊 + # 那梦又是多么的真实…… + if r"%%t" in pgs_style: result.append( SingleCommand( @@ -552,13 +557,12 @@ class MidiConvert: 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) - + print(msg, E) + if msg.type == "program_change": midi_channels[msg.channel][track_no].append( ("PgmC", msg.program, microseconds) @@ -576,7 +580,6 @@ class MidiConvert: ("NoteE", msg.note, microseconds) ) - """整合后的音乐通道格式 每个通道包括若干消息元素其中逃不过这三种: @@ -689,7 +692,7 @@ class MidiConvert: max_volume: float = 1.0, speed: float = 1.0, player_selector: str = "@a", - ) -> Tuple[List[SingleCommand], int]: + ) -> Tuple[List[SingleCommand], int, int]: """ 使用金羿的转换思路,将midi转换为我的世界命令列表,并输出每个音符之后的延迟 @@ -704,7 +707,7 @@ class MidiConvert: Returns ------- - tuple( list[SingleCommand,...], int音乐时长游戏刻 ) + tuple( list[SingleCommand,...], int音乐时长游戏刻, int最大同时播放的指令数量 ) """ if speed == 0: @@ -740,10 +743,10 @@ class MidiConvert: else self.inst_to_souldID_withX(InstID) ) - score_now = round(msg[-1] / float(speed) / 50) + delaytime_now = round(msg[-1] / float(speed) / 50) try: - tracks[score_now].append( + tracks[delaytime_now].append( self.execute_cmd_head.format(player_selector) + f"playsound {soundID} @s ^ ^ ^{128 / max_volume / msg[2] - 1} {msg[2] / 128} " + ( @@ -753,7 +756,7 @@ class MidiConvert: ) ) except KeyError: - tracks[score_now] = [ + tracks[delaytime_now] = [ self.execute_cmd_head.format(player_selector) + f"playsound {soundID} @s ^ ^ ^{128 / max_volume / msg[2] - 1} {msg[2] / 128} " + ( @@ -771,8 +774,10 @@ class MidiConvert: all_ticks = list(tracks.keys()) all_ticks.sort() results = [] + max_multi = 0 for i in range(len(all_ticks)): + max_multi = max(max_multi, len(tracks[all_ticks[i]])) for j in range(len(tracks[all_ticks[i]])): results.append( SingleCommand( @@ -794,7 +799,7 @@ class MidiConvert: self.music_command_list = results self.music_tick_num = max(all_ticks) - return results, self.music_tick_num + return results, self.music_tick_num, max_multi def copy_important(self): dst = MidiConvert( diff --git a/Musicreater/plugin/bdx.py b/Musicreater/plugin/bdx.py index b2d4f11..46b4a11 100644 --- a/Musicreater/plugin/bdx.py +++ b/Musicreater/plugin/bdx.py @@ -148,7 +148,6 @@ def commands_to_BDX_bytes( now_x = 0 for command in commands_list: - _bytes += form_command_block_in_BDX_bytes( command.command_text, (1 if y_forward else 0) @@ -156,12 +155,14 @@ def commands_to_BDX_bytes( ((now_y != 0) and (not y_forward)) or (y_forward and (now_y != (max_height - 1))) ) - else (3 if z_forward else 2) - if ( - ((now_z != 0) and (not z_forward)) - or (z_forward and (now_z != _sideLength - 1)) - ) - else 5, + else ( + (3 if z_forward else 2) + if ( + ((now_z != 0) and (not z_forward)) + or (z_forward and (now_z != _sideLength - 1)) + ) + else 5 + ), impluse=2, condition=command.conditional, needRedstone=False, @@ -171,6 +172,16 @@ def commands_to_BDX_bytes( trackOutput=True, ) + # (1 if y_forward else 0) if ( # 如果y+则向上,反之向下 + # ((now_y != 0) and (not y_forward)) # 如果不是y轴上首个方块 + # or (y_forward and (now_y != (max_height - 1))) # 如果不是y轴上末端方块 + # ) else ( # 否则,即是y轴末端或首个方块 + # (3 if z_forward else 2) if ( # 如果z+则向z轴正方向,反之负方向 + # ((now_z != 0) and (not z_forward)) # 如果不是z轴上的首个方块 + # or (z_forward and (now_z != _sideLength - 1)) # 如果不是z轴上的末端方块 + # ) else 5 # 否则,则要面向x轴正方向 + # ) + now_y += 1 if y_forward else -1 if ((now_y >= max_height) and y_forward) or ((now_y < 0) and (not y_forward)): diff --git a/Musicreater/plugin/bdxfile/main.py b/Musicreater/plugin/bdxfile/main.py index 7d7694c..a4b4253 100644 --- a/Musicreater/plugin/bdxfile/main.py +++ b/Musicreater/plugin/bdxfile/main.py @@ -154,7 +154,7 @@ def to_BDX_file_in_delay( data_cfg.volume_ratio, data_cfg.speed_multiplier, player, - ) + )[:2] if not os.path.exists(data_cfg.dist_path): os.makedirs(data_cfg.dist_path) diff --git a/Musicreater/plugin/common.py b/Musicreater/plugin/common.py index 2aff4a6..bd08bf5 100644 --- a/Musicreater/plugin/common.py +++ b/Musicreater/plugin/common.py @@ -25,3 +25,5 @@ def bottem_side_length_of_smallest_square_bottom_box(total: int, maxHeight: int) :param maxHeight: 最大高度 :return: 外切正方形的边长 int""" return math.ceil(math.sqrt(math.ceil(total / maxHeight))) + + diff --git a/Musicreater/plugin/mcstructfile/__init__.py b/Musicreater/plugin/mcstructfile/__init__.py index 80f445b..70a1bd5 100644 --- a/Musicreater/plugin/mcstructfile/__init__.py +++ b/Musicreater/plugin/mcstructfile/__init__.py @@ -15,7 +15,8 @@ Terms & Conditions: License.md in the root directory __all__ = [ - "to_mcstructure_file_in_delay" + "to_mcstructure_file_in_delay", + "to_mcstructure_file_in_redstone_CD", ] __author__ = (("金羿", "Eilles Wan"),) diff --git a/Musicreater/plugin/mcstructfile/main.py b/Musicreater/plugin/mcstructfile/main.py index 3541793..e62f96b 100644 --- a/Musicreater/plugin/mcstructfile/main.py +++ b/Musicreater/plugin/mcstructfile/main.py @@ -13,10 +13,12 @@ Terms & Conditions: License.md in the root directory import os +from typing import Literal + from ...exceptions import CommandFormatError from ...main import MidiConvert from ..main import ConvertConfig -from ..mcstructure import commands_to_structure +from ..mcstructure import commands_to_structure,commands_to_redstone_delay_structure def to_mcstructure_file_in_delay( @@ -51,7 +53,7 @@ def to_mcstructure_file_in_delay( data_cfg.volume_ratio, data_cfg.speed_multiplier, player, - ) + )[:2] if not os.path.exists(data_cfg.dist_path): os.makedirs(data_cfg.dist_path) @@ -69,8 +71,56 @@ def to_mcstructure_file_in_delay( return size, max_delay -def to_mcstructure_file_in_redstone( + +def to_mcstructure_file_in_redstone_CD( midi_cvt: MidiConvert, data_cfg: ConvertConfig, + player: str = "@a", + axis_side: Literal["z+","z-","Z+","Z-","x+","x-","X+","X-"] = "z+", + basement_block: str = "concrete", ): - pass + """ + 将midi以延迟播放器形式转换为mcstructure结构文件 + + Parameters + ---------- + midi_cvt: MidiConvert 对象 + 用于转换的MidiConvert对象 + data_cfg: ConvertConfig 对象 + 部分转换通用参数 + player: str + 玩家选择器,默认为`@a` + axis_side: Literal["z+","z-","Z+","Z-","x+","x-","X+","X-"] + 生成结构的延展方向 + basement_block: str + 结构的基底方块 + + Returns + ------- + tuple[tuple[int,]结构大小, int音乐总延迟] + """ + + if midi_cvt.enable_old_exe_format: + raise CommandFormatError("使用mcstructure结构文件导出时不支持旧版本的指令格式。") + + cmd_list, max_delay, max_multiple_cmd = midi_cvt.to_command_list_in_delay( + data_cfg.volume_ratio, + data_cfg.speed_multiplier, + player, + ) + + if not os.path.exists(data_cfg.dist_path): + os.makedirs(data_cfg.dist_path) + + struct, size, end_pos = commands_to_redstone_delay_structure(cmd_list,max_delay,max_multiple_cmd, basement_block, axis_side) + + with open( + os.path.abspath( + os.path.join(data_cfg.dist_path, f"{midi_cvt.midi_music_name}.mcstructure") + ), + "wb+", + ) as f: + struct.dump(f) + + return size, max_delay + diff --git a/Musicreater/plugin/mcstructpack/main.py b/Musicreater/plugin/mcstructpack/main.py index 9e45a5f..9fb3a8c 100644 --- a/Musicreater/plugin/mcstructpack/main.py +++ b/Musicreater/plugin/mcstructpack/main.py @@ -56,7 +56,7 @@ def to_mcstructure_addon_in_delay( data_cfg.volume_ratio, data_cfg.speed_multiplier, player, - ) + )[:2] if not os.path.exists(data_cfg.dist_path): os.makedirs(data_cfg.dist_path) diff --git a/Musicreater/plugin/mcstructure.py b/Musicreater/plugin/mcstructure.py index 1828e79..79a84e7 100644 --- a/Musicreater/plugin/mcstructure.py +++ b/Musicreater/plugin/mcstructure.py @@ -16,12 +16,40 @@ Terms & Conditions: License.md in the root directory # 若需转载或借鉴 许可声明请查看仓库目录下的 License.md -from typing import List, Tuple +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 .common import bottem_side_length_of_smallest_square_bottom_box +import struct + + +def antiaxis(axis: Literal['x','z','X','Z']): + return z if axis == x else x + + +def forward_IER(forward: bool): + return 1 if forward else -1 + +AXIS_PARTICULAR_VALUE = { + x: { + True: 5, + False: 4, + }, + y: { + True: 1, + False: 0, + }, + z: { + True: 3, + False: 2, + }, +} + +def command_statevalue(axis_: Literal['x','y','z','X','Y','Z'],forward_:bool): + return AXIS_PARTICULAR_VALUE[axis_.lower()][forward_] def form_note_block_in_NBT_struct( @@ -64,8 +92,12 @@ def form_note_block_in_NBT_struct( def form_repeater_in_NBT_struct(delay: int, facing: int): """生成中继器方块 - :param facing: - :param delay: 1~4 + :param facing: 朝向: + Z- 北 0 + X- 东 1 + Z+ 南 2 + X+ 西 3 + :param delay: 0~3 :return Block()""" return Block( @@ -165,7 +197,7 @@ def form_command_block_in_NBT_struct( "x": coordinate[0], "y": coordinate[1], "z": coordinate[2], - } # type: ignore + } # type: ignore }, compability_version=17959425, ) @@ -251,3 +283,109 @@ def commands_to_structure( ), (now_x, now_y, now_z), ) + +def commands_to_redstone_delay_structure( + commands: List[SingleCommand], + delay_length: int, + max_multicmd_length: int, + base_block: str = "concrete", + axis_: Literal["z+","z-","Z+","Z-","x+","x-","X+","X-"] = "z+", +) -> Tuple[Structure, Tuple[int, int, int], Tuple[int, int, int]]: + """ + :param commands: 指令列表 + :param delay_length: 延时总长 + :param max_multicmd_length: 最大同时播放的音符数量 + :param base_block: 生成结构的基底方块 + :param axis_: 生成结构的延展方向 + :return 结构类,结构占用大小,终点坐标 + """ + if axis_ in ["z+", "Z+"]: + extensioon_direction = z + aside_direction = x + repeater_facing = 2 + forward = True + elif axis_ in ["z-", "Z-"]: + extensioon_direction = z + aside_direction = x + repeater_facing = 0 + forward = False + elif axis_ in ["x+", "X+"]: + extensioon_direction = x + aside_direction = z + repeater_facing = 3 + forward = True + elif axis_ in ["x-", "X-"]: + extensioon_direction = x + aside_direction = z + repeater_facing = 1 + forward = False + else: + raise ValueError(f"axis_({axis_}) 参数错误。") + + + goahead = forward_IER(forward) + + struct = Structure( + (round(delay_length/2+0.5+len(commands)) if extensioon_direction == x else max_multicmd_length, 3, round(delay_length/2+0.5+len(commands)) if extensioon_direction == z else max_multicmd_length) + ) + + pos_now = {x:(0 if forward else struct.size[0]),y:0,z:(0 if forward else struct.size[2])} + + first_impluse = True + + for cmd in commands: + single_repeater_value = round(cmd.delay / 2) % 4 - 1 + additional_repeater = round(cmd.delay / 2)// 4 + for i in range(additional_repeater): + struct.set_block( + tuple(pos_now.values()), + Block("minecraft", base_block,), + ) + struct.set_block( + (pos_now[x],1,pos_now[z]), + form_repeater_in_NBT_struct( + delay=3, + facing=repeater_facing, + ), + ) + pos_now[extensioon_direction] += goahead + first_impluse = True + if single_repeater_value >= 0: + struct.set_block( + tuple(pos_now.values()), + Block("minecraft", base_block,), + ) + struct.set_block( + (pos_now[x],1,pos_now[z]), + form_repeater_in_NBT_struct( + delay=single_repeater_value, + facing=repeater_facing, + ), + ) + pos_now[extensioon_direction] += goahead + first_impluse = True + struct.set_block( + (pos_now[x],1,pos_now[z]), + form_command_block_in_NBT_struct( + command=cmd.command_text, + coordinate=(pos_now[x],1,pos_now[z]), + particularValue=command_statevalue(extensioon_direction, forward), + # impluse= (0 if first_impluse else 2), + impluse=0, + condition=False, + alwaysRun=False, + tickDelay=0, + customName=cmd.annotation_text, + ), + ) + struct.set_block( + (pos_now[x],2,pos_now[z]), + Block("minecraft", "redstone_wire"), + ) + pos_now[extensioon_direction] += goahead + + first_impluse = False + + return struct, struct.size, tuple(pos_now.values()) + + diff --git a/Musicreater/plugin/schematic.py b/Musicreater/plugin/schematic.py new file mode 100644 index 0000000..82d086c --- /dev/null +++ b/Musicreater/plugin/schematic.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +""" +存放有关Schematic结构生成的内容 +""" + +""" +版权所有 © 2023 音·创 开发者 +Copyright © 2023 all the developers of Musicreater + +开源相关声明请见 仓库根目录下的 License.md +Terms & Conditions: License.md in the root directory +""" + +# 睿穆组织 开发交流群 861684859 +# Email TriM-Organization@hotmail.com +# 若需转载或借鉴 许可声明请查看仓库目录下的 License.md + +import nbtschematic + diff --git a/Musicreater/plugin/schematic/__init__.py b/Musicreater/plugin/schematic/__init__.py new file mode 100644 index 0000000..edef3bd --- /dev/null +++ b/Musicreater/plugin/schematic/__init__.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +""" +用以生成Schematic结构的附加功能 + +版权所有 © 2023 音·创 开发者 +Copyright © 2023 all the developers of Musicreater + +开源相关声明请见 仓库根目录下的 License.md +Terms & Conditions: License.md in the root directory +""" + +# 睿穆组织 开发交流群 861684859 +# Email TriM-Organization@hotmail.com +# 若需转载或借鉴 许可声明请查看仓库目录下的 License.md + + +__all__ = [ +] +__author__ = (("金羿", "Eilles Wan"),) + +from .main import * + diff --git a/Musicreater/plugin/schematic/main.py b/Musicreater/plugin/schematic/main.py new file mode 100644 index 0000000..728ab9b --- /dev/null +++ b/Musicreater/plugin/schematic/main.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +""" +版权所有 © 2023 音·创 开发者 +Copyright © 2023 all the developers of Musicreater + +开源相关声明请见 仓库根目录下的 License.md +Terms & Conditions: License.md in the root directory +""" + +# 睿穆组织 开发交流群 861684859 +# Email TriM-Organization@hotmail.com +# 若需转载或借鉴 许可声明请查看仓库目录下的 License.md + +from ..schematic import * \ No newline at end of file diff --git a/Musicreater/previous.py b/Musicreater/previous.py index f2aeb38..b9d5a07 100644 --- a/Musicreater/previous.py +++ b/Musicreater/previous.py @@ -98,6 +98,63 @@ class ObsoleteMidiConvert(MidiConvert): 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): + + 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) + ) + maxscore = max(maxscore, nowscore) + soundID, _X = self.__Inst2soundID_withX(instrumentID) + 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: MidiConvert, diff --git a/Musicreater/subclass.py b/Musicreater/subclass.py index baea371..e20b9b0 100644 --- a/Musicreater/subclass.py +++ b/Musicreater/subclass.py @@ -18,6 +18,9 @@ Terms & Conditions: License.md in the root directory from dataclasses import dataclass +from typing import Optional + +from .constants import PERCUSSION_INSTRUMENT_LIST @dataclass(init=False) @@ -43,7 +46,13 @@ class SingleNote: """音符所处的音轨""" def __init__( - self, instrument: int, pitch: int, velocity: int, startTime: int, lastTime: int, track_number:int = 0 + self, + instrument: int, + pitch: int, + velocity: int, + startTime: int, + lastTime: int, + track_number: int = 0, ): """用于存储单个音符的类 :param instrument 乐器编号 @@ -97,6 +106,11 @@ class SingleNote: "lastTime": self.duration, } + def __eq__(self, other) -> bool: + if not isinstance(other, self.__class__): + return False + return self.__str__() == other.__str__() + @dataclass(init=False) class SingleCommand: @@ -170,3 +184,96 @@ class SingleCommand: if not isinstance(other, self.__class__): return False return self.__str__() == other.__str__() + + +@dataclass(init=False) +class SingleNoteBox: + """存储单个音符盒""" + + instrument_block: str + """乐器方块""" + + note_value: int + """音符盒音高""" + + annotation_text: str + """音符注释""" + + is_percussion: bool + """是否为打击乐器""" + + def __init__( + self, + instrument_block_: str, + note_value_: int, + percussion: Optional[bool] = None, + annotation: str = "", + ): + """用于存储单个音符盒的类 + :param instrument_block_ 音符盒演奏所使用的乐器方块 + :param note_value_ 音符盒的演奏音高 + :param percussion 此音符盒乐器是否作为打击乐处理 + 注:若为空,则自动识别是否为打击乐器 + :param annotation 音符注释""" + self.instrument_block = instrument_block_ + """乐器方块""" + self.note_value = note_value_ + """音符盒音高""" + self.annotation_text = annotation + """音符注释""" + if percussion is None: + self.is_percussion = percussion in PERCUSSION_INSTRUMENT_LIST + else: + self.is_percussion = percussion + + @property + def inst(self) -> str: + """获取音符盒下的乐器方块""" + return self.instrument_block + + @inst.setter + def inst(self, inst_): + self.instrument_block = inst_ + + @property + def note(self) -> int: + """获取音符盒音调特殊值""" + return self.note_value + + @note.setter + def note(self, note_): + self.note_value = note_ + + @property + def annotation(self) -> str: + """获取音符盒的备注""" + return self.annotation_text + + @annotation.setter + def annotation(self, annotation_): + self.annotation_text = annotation_ + + def copy(self): + return SingleNoteBox( + instrument_block_=self.instrument_block, + note_value_=self.note_value, + annotation=self.annotation_text, + ) + + def __str__(self) -> str: + return f"Note(inst = {self.inst}, note = {self.note}, )" + + def __tuple__(self) -> tuple: + return self.inst, self.note, self.annotation + + def __dict__(self) -> dict: + return { + "inst": self.inst, + "note": self.note, + "annotation": self.annotation, + } + + def __eq__(self, other) -> bool: + if not isinstance(other, self.__class__): + return False + return self.__str__() == other.__str__() diff --git a/RSMC_test.py b/RSMC_test.py new file mode 100644 index 0000000..08c1ea4 --- /dev/null +++ b/RSMC_test.py @@ -0,0 +1,8 @@ +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_mcstructure_rcd.py b/example_mcstructure_rcd.py new file mode 100644 index 0000000..941a828 --- /dev/null +++ b/example_mcstructure_rcd.py @@ -0,0 +1,13 @@ +import Musicreater +import Musicreater.plugin +import Musicreater.plugin.mcstructfile + +print( + Musicreater.plugin.mcstructfile.to_mcstructure_file_in_redstone_CD( + Musicreater.MidiConvert.from_midi_file(input("midi路径:"), old_exe_format=False), + Musicreater.plugin.ConvertConfig( + input("输出路径:"), + volume=1 + ), + ) +) diff --git a/requirements.txt b/requirements.txt index 55434ba..dab89c8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1 @@ -LyricLib>=0.0.3 mido>=1.3