diff --git a/Musicreater/instConstants.py b/Musicreater/constants.py similarity index 87% rename from Musicreater/instConstants.py rename to Musicreater/constants.py index 8ddbd40..7bb6b27 100644 --- a/Musicreater/instConstants.py +++ b/Musicreater/constants.py @@ -1,4 +1,32 @@ -pitched_instrument_list = { +""" +存放常量与数值性内容 +""" + + +x = "x" +""" +x +""" + +y = "y" +""" +y +""" + +z = "z" +""" +z +""" + +DEFAULT_PROGRESSBAR_STYLE = ( + r"▶ %%N [ %%s/%^s %%% __________ %%t|%^t ]", + ("§e=§r", "§7=§r"), +) +""" +默认的进度条样式组 +""" + +PITCHED_INSTRUMENT_LIST = { 0: ("note.harp", 6), 1: ("note.harp", 6), 2: ("note.pling", 6), @@ -129,7 +157,7 @@ pitched_instrument_list = { 127: ("note.snare", 7), # 打击乐器无音域 } -percussion_instrument_list = { +PERCUSSION_INSTRUMENT_LIST = { 34: ("note.bd", 7), 35: ("note.bd", 7), 36: ("note.hat", 7), @@ -179,7 +207,7 @@ percussion_instrument_list = { 80: ("note.bell", 4), } -instrument_to_blocks_list = { +INSTRUMENT_BLOCKS_LIST = { "note.bass": ("planks",), "note.snare": ("sand",), "note.hat": ("glass",), @@ -198,3 +226,35 @@ instrument_to_blocks_list = { "note.bassattack": ("command_block",), # 无法找到此音效 "note.harp": ("glass",), } + + +# 即将启用 +height2note = { + 0.5: 0, + 0.53: 1, + 0.56: 2, + 0.6: 3, + 0.63: 4, + 0.67: 5, + 0.7: 6, + 0.75: 7, + 0.8: 8, + 0.84: 9, + 0.9: 10, + 0.94: 11, + 1.0: 12, + 1.05: 13, + 1.12: 14, + 1.2: 15, + 1.25: 16, + 1.33: 17, + 1.4: 18, + 1.5: 19, + 1.6: 20, + 1.7: 21, + 1.8: 22, + 1.9: 23, + 2.0: 24, +} +"""音高对照表\n +MC音高:音符盒音调""" diff --git a/Musicreater/experiment.py b/Musicreater/experiment.py new file mode 100644 index 0000000..a41da8e --- /dev/null +++ b/Musicreater/experiment.py @@ -0,0 +1,358 @@ +''' +新版本功能以及即将启用的函数 +''' + + + +from .exceptions import * +from .subclass import * +from .utils import * +from .main import midiConvert, mido + + +# 简单的单音填充 +def _toCmdList_m4( + self: midiConvert, + scoreboard_name: str = "mscplay", + MaxVolume: float = 1.0, + speed: float = 1.0, +) -> list: + """ + 使用金羿的转换思路,将midi转换为我的世界命令列表,并使用完全填充算法优化音感 + :param scoreboard_name: 我的世界的计分板名称 + :param MaxVolume: 音量,注意:这里的音量范围为(0,1],如果超出将被处理为正确值,其原理为在距离玩家 (1 / volume -1) 的地方播放音频 + :param speed: 速度,注意:这里的速度指的是播放倍率,其原理为在播放音频的时候,每个音符的播放时间除以 speed + :return: tuple(命令列表, 命令个数, 计分板最大值) + """ + # TODO: 这里的时间转换不知道有没有问题 + + if speed == 0: + if self.debug_mode: + raise ZeroSpeedError("播放速度仅可为正实数") + speed = 1 + MaxVolume = 1 if MaxVolume > 1 else (0.001 if MaxVolume <= 0 else MaxVolume) + + # 一个midi中仅有16个通道 我们通过通道来识别而不是音轨 + channels = [[], [], [], [], [], [], [], [], [], [], [], [], [], [], [], []] + + # 我们来用通道统计音乐信息 + for i, track in enumerate(self.midi.tracks): + microseconds = 0 + + for msg in track: + if msg.time != 0: + try: + microseconds += msg.time * tempo / self.midi.ticks_per_beat + except NameError: + raise NotDefineTempoError("计算当前分数时出错 未定义参量 Tempo") + + if msg.is_meta: + if msg.type == "set_tempo": + tempo = msg.tempo + else: + if self.debug_mode: + try: + if msg.channel > 15: + raise ChannelOverFlowError(f"当前消息 {msg} 的通道超限(≤15)") + except AttributeError: + pass + + if msg.type == "program_change": + channels[msg.channel].append( + ("PgmC", msg.program, microseconds) + ) + + elif msg.type == "note_on" and msg.velocity != 0: + channels[msg.channel].append( + ("NoteS", msg.note, msg.velocity, microseconds) + ) + + elif (msg.type == "note_on" and msg.velocity == 0) or ( + msg.type == "note_off" + ): + channels[msg.channel].append(("NoteE", msg.note, microseconds)) + + """整合后的音乐通道格式 + 每个通道包括若干消息元素其中逃不过这三种: + + 1 切换乐器消息 + + ("PgmC", 切换后的乐器ID: int, 距离演奏开始的毫秒) + + 2 音符开始消息 + + ("NoteS", 开始的音符ID, 力度(响度), 距离演奏开始的毫秒) + + 3 音符结束消息 + + ("NoteS", 结束的音符ID, 距离演奏开始的毫秒)""" + + note_channels = [[], [], [], [], [], [], [], [], [], [], [], [], [], [], [], []] + + # 此处 我们把通道视为音轨 + for i in range(len(channels)): + # 如果当前通道为空 则跳过 + + noteMsgs = [] + MsgIndex = [] + + for msg in channels[i]: + if msg[0] == "PgmC": + InstID = msg[1] + + elif msg[0] == "NoteS": + noteMsgs.append(msg[1:]) + MsgIndex.append(msg[1]) + + elif msg[0] == "NoteE": + if msg[1] in MsgIndex: + note_channels[i].append( + SingleNote( + InstID, + msg[1], + noteMsgs[MsgIndex.index(msg[1])][1], + noteMsgs[MsgIndex.index(msg[1])][2], + msg[-1] - noteMsgs[MsgIndex.index(msg[1])][2], + ) + ) + noteMsgs.pop(MsgIndex.index(msg[1])) + MsgIndex.pop(MsgIndex.index(msg[1])) + + tracks = [] + cmdAmount = 0 + maxScore = 0 + CheckFirstChannel = False + + # 临时用的插值计算函数 + def _linearFun(_note: SingleNote) -> list: + """传入音符数据,返回以半秒为分割的插值列表 + :param _note: SingleNote 音符 + :return list[tuple(int开始时间(毫秒), int乐器, int音符, int力度(内置), float音量(播放)),]""" + + result = [] + + totalCount = int(_note.lastTime / 500) + + for _i in range(totalCount): + result.append( + ( + _note.startTime + _i * 500, + _note.instrument, + _note.pitch, + _note.velocity, + MaxVolume * ((totalCount - _i) / totalCount), + ) + ) + + return result + + # 此处 我们把通道视为音轨 + for track in note_channels: + # 如果当前通道为空 则跳过 + if not track: + continue + + if note_channels.index(track) == 0: + CheckFirstChannel = True + SpecialBits = False + elif note_channels.index(track) == 9: + SpecialBits = True + else: + CheckFirstChannel = False + SpecialBits = False + + nowTrack = [] + + for note in track: + for every_note in _linearFun(note): + # 应该是计算的时候出了点小问题 + # 我们应该用一个MC帧作为时间单位而不是半秒 + + if SpecialBits: + soundID, _X = self.perc_inst_to_soundID_withX(InstID) + else: + soundID, _X = self.inst_to_souldID_withX(InstID) + + score_now = round(every_note[0] / speed / 50000) + + maxScore = max(maxScore, score_now) + + nowTrack.append( + "execute @a[scores={" + + str(scoreboard_name) + + "=" + + str(score_now) + + "}" + + f"] ~ ~ ~ playsound {soundID} @s ~ ~{1 / every_note[4] - 1} ~ " + f"{note.velocity * (0.7 if CheckFirstChannel else 0.9)} {2 ** ((note.pitch - 60 - _X) / 12)}" + ) + + cmdAmount += 1 + tracks.append(nowTrack) + + return [tracks, cmdAmount, maxScore] + + + +def to_note_list( + self, + speed: float = 1.0, +) -> list: + """ + 使用金羿的转换思路,将midi转换为我的世界音符盒所用的音高列表,并输出每个音符之后的延迟 + + Parameters + ---------- + speed: float + 速度,注意:这里的速度指的是播放倍率,其原理为在播放音频的时候,每个音符的播放时间除以 speed + + Returns + ------- + tuple( list[tuple(str指令, int距离上一个指令的延迟 ),...], int音乐时长游戏刻 ) + """ + + if speed == 0: + if self.debug_mode: + raise ZeroSpeedError("播放速度仅可为正实数") + speed = 1 + + # 一个midi中仅有16个通道 我们通过通道来识别而不是音轨 + channels = empty_midi_channels() + + # 我们来用通道统计音乐信息 + # 但是是用分轨的思路的 + for track_no, track in enumerate(self.midi.tracks): + microseconds = 0 + + for msg in track: + if msg.time != 0: + try: + microseconds += ( + msg.time * tempo / self.midi.ticks_per_beat / 1000 + ) + # print(microseconds) + except NameError: + if self.debug_mode: + raise NotDefineTempoError("计算当前分数时出错 未定义参量 Tempo") + else: + microseconds += ( + msg.time + * mido.midifiles.midifiles.DEFAULT_TEMPO + / self.midi.ticks_per_beat + ) / 1000 + + if msg.is_meta: + if msg.type == "set_tempo": + tempo = msg.tempo + if self.debug_mode: + self.prt(f"TEMPO更改:{tempo}(毫秒每拍)") + else: + try: + if msg.channel > 15 and self.debug_mode: + raise ChannelOverFlowError(f"当前消息 {msg} 的通道超限(≤15)") + if not track_no in channels[msg.channel].keys(): + channels[msg.channel][track_no] = [] + except AttributeError: + pass + + if msg.type == "program_change": + channels[msg.channel][track_no].append( + ("PgmC", msg.program, microseconds) + ) + + elif msg.type == "note_on" and msg.velocity != 0: + 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" + ): + channels[msg.channel][track_no].append( + ("NoteE", msg.note, microseconds) + ) + + """整合后的音乐通道格式 + 每个通道包括若干消息元素其中逃不过这三种: + + 1 切换乐器消息 + ("PgmC", 切换后的乐器ID: int, 距离演奏开始的毫秒) + + 2 音符开始消息 + ("NoteS", 开始的音符ID, 力度(响度), 距离演奏开始的毫秒) + + 3 音符结束消息 + ("NoteS", 结束的音符ID, 距离演奏开始的毫秒)""" + + tracks = {} + + # 此处 我们把通道视为音轨 + for i in channels.keys(): + # 如果当前通道为空 则跳过 + if not channels[i]: + continue + + # 第十通道是打击乐通道 + SpecialBits = True if i == 9 else False + + # nowChannel = [] + + for track_no, track in channels[i].items(): + for msg in track: + if msg[0] == "PgmC": + InstID = msg[1] + + elif msg[0] == "NoteS": + try: + soundID, _X = ( + self.perc_inst_to_soundID_withX(InstID) + if SpecialBits + else self.inst_to_souldID_withX(InstID) + ) + except UnboundLocalError as E: + if self.debug_mode: + raise NotDefineProgramError(f"未定义乐器便提前演奏。\n{E}") + else: + soundID, _X = ( + self.perc_inst_to_soundID_withX(-1) + if SpecialBits + else self.inst_to_souldID_withX(-1) + ) + score_now = round(msg[-1] / float(speed) / 50) + # print(score_now) + + 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() + results = [] + + 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)] \ No newline at end of file diff --git a/Musicreater/magicmain.py b/Musicreater/magicmain.py index b066390..fa657d8 100644 --- a/Musicreater/magicmain.py +++ b/Musicreater/magicmain.py @@ -212,7 +212,7 @@ if __name__ == '__main__': from typing import Union -from .utils import x,y,z,bottem_side_length_of_smallest_square_bottom_box,form_note_block_in_NBT_struct,form_repeater_in_NBT_struct +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 # 不要用 没写完 def delay_to_note_blocks( @@ -243,63 +243,54 @@ def delay_to_note_blocks( # 1拍 x 2.5 rt - def placeNoteBlock(): - for i in notes: - error = True - try: - struct.set_block( - [startpos[0], startpos[1] + 1, startpos[2]], - form_note_block_in_NBT_struct(height2note[i[0]], instrument), - ) - struct.set_block(startpos, Block("universal_minecraft", instuments[i[0]][1]),) - error = False - except ValueError: + for i in notes: + error = True + try: + struct.set_block( + [startpos[0], startpos[1] + 1, startpos[2]], + form_note_block_in_NBT_struct(height2note[i[0]], instrument), + ) + struct.set_block(startpos, Block("universal_minecraft", instuments[i[0]][1]),) + error = False + except ValueError: + log("无法放置音符:" + str(i) + "于" + str(startpos)) + struct.set_block(Block("universal_minecraft", baseblock), startpos) + struct.set_block( + Block("universal_minecraft", baseblock), + [startpos[0], startpos[1] + 1, startpos[2]], + ) + finally: + if error is True: log("无法放置音符:" + str(i) + "于" + str(startpos)) struct.set_block(Block("universal_minecraft", baseblock), startpos) struct.set_block( Block("universal_minecraft", baseblock), [startpos[0], startpos[1] + 1, startpos[2]], ) - finally: - if error is True: - log("无法放置音符:" + str(i) + "于" + str(startpos)) - struct.set_block(Block("universal_minecraft", baseblock), startpos) - struct.set_block( - Block("universal_minecraft", baseblock), - [startpos[0], startpos[1] + 1, startpos[2]], - ) - delay = int(i[1] * speed + 0.5) - if delay <= 4: + delay = int(i[1] * speed + 0.5) + if delay <= 4: + startpos[0] += 1 + struct.set_block( + form_repeater_in_NBT_struct(delay, "west"), + [startpos[0], startpos[1] + 1, startpos[2]], + ) + struct.set_block(Block("universal_minecraft", baseblock), startpos) + else: + for j in range(int(delay / 4)): startpos[0] += 1 struct.set_block( - form_repeater_in_NBT_struct(delay, "west"), + form_repeater_in_NBT_struct(4, "west"), [startpos[0], startpos[1] + 1, startpos[2]], ) struct.set_block(Block("universal_minecraft", baseblock), startpos) - else: - for j in range(int(delay / 4)): - startpos[0] += 1 - struct.set_block( - form_repeater_in_NBT_struct(4, "west"), - [startpos[0], startpos[1] + 1, startpos[2]], - ) - struct.set_block(Block("universal_minecraft", baseblock), startpos) - if delay % 4 != 0: - startpos[0] += 1 - struct.set_block( - form_repeater_in_NBT_struct(delay % 4, "west"), - [startpos[0], startpos[1] + 1, startpos[2]], - ) - struct.set_block(Block("universal_minecraft", baseblock), startpos) - startpos[0] += posadder[0] - startpos[1] += posadder[1] - startpos[2] += posadder[2] - - # e = True - try: - placeNoteBlock() - # e = False - except: # ValueError - log("无法放置方块了,可能是因为区块未加载叭") - + if delay % 4 != 0: + startpos[0] += 1 + struct.set_block( + form_repeater_in_NBT_struct(delay % 4, "west"), + [startpos[0], startpos[1] + 1, startpos[2]], + ) + struct.set_block(Block("universal_minecraft", baseblock), startpos) + startpos[0] += posadder[0] + startpos[1] += posadder[1] + startpos[2] += posadder[2] diff --git a/Musicreater/main.py b/Musicreater/main.py index 5f77c32..22d7d94 100644 --- a/Musicreater/main.py +++ b/Musicreater/main.py @@ -20,96 +20,24 @@ Copyright © 2023 all the developers of Musicreater Terms & Conditions: ../License.md """ +import os +import math import json import shutil import uuid -from typing import TypeVar, Union +from typing import TypeVar, Union, Tuple -import brotli import mido from .exceptions import * -from .instConstants import * +from .constants import * from .utils import * +from .subclass import * -T = TypeVar("T") # Declare type variable VM = TypeVar("VM", mido.MidiFile, None) # void mido - -DEFAULT_PROGRESSBAR_STYLE = ( - r"▶ %%N [ %%s/%^s %%% __________ %%t|%^t ]", - ("§e=§r", "§7=§r"), -) - - -class SingleNote: - def __init__( - self, instrument: int, pitch: int, velocity: int, startTime: int, lastTime: int - ): - """用于存储单个音符的类 - :param instrument 乐器编号 - :param pitch 音符编号 - :param velocity 力度/响度 - :param startTime 开始之时(ms) - 注:此处的时间是用从乐曲开始到当前的毫秒数 - :param lastTime 音符延续时间(ms)""" - self.instrument: int = instrument - """乐器编号""" - self.note: int = pitch - """音符编号""" - self.velocity: int = velocity - """力度/响度""" - self.startTime: int = startTime - """开始之时 ms""" - self.lastTime: int = lastTime - """音符持续时间 ms""" - - @property - def inst(self): - """乐器编号""" - return self.instrument - - @inst.setter - def inst(self, inst_): - self.instrument = inst_ - - @property - def pitch(self): - """音符编号""" - return self.note - - def __str__(self): - return ( - f"Note(inst = {self.inst}, pitch = {self.note}, velocity = {self.velocity}, " - f"startTime = {self.startTime}, lastTime = {self.lastTime}, )" - ) - - def __tuple__(self): - return self.inst, self.note, self.velocity, self.startTime, self.lastTime - - def __dict__(self): - return { - "inst": self.inst, - "pitch": self.note, - "velocity": self.velocity, - "startTime": self.startTime, - "lastTime": self.lastTime, - } - - -class MethodList(list): - """函数列表,列表中的所有元素均为函数""" - - def __init__(self, in_=()): - """函数列表,列表中的所有元素均为函数""" - super().__init__() - self._T = [_x for _x in in_] - - def __getitem__(self, item) -> T: - return self._T[item] - - def __len__(self) -> int: - return self._T.__len__() - +''' +空Midi类类型 +''' """ 学习笔记: @@ -146,8 +74,17 @@ tick * tempo / 1000000.0 / ticks_per_beat * 一秒多少游戏刻 class midiConvert: - def __init__(self, enable_old_exe_format: bool = True, debug: bool = False): - """简单的midi转换类,将midi文件转换为我的世界结构或者包""" + def __init__(self, enable_old_exe_format: bool = False, debug: bool = False): + """ + 简单的midi转换类,将midi文件转换为我的世界结构或者包 + + Parameters + ---------- + enable_old_exe_format: bool + 是否启用旧版(≤1.19)指令格式,默认为否 + debug: bool + 是否启用调试模式,默认为否 + """ self.debug_mode: bool = debug """是否开启调试模式""" @@ -167,25 +104,6 @@ class midiConvert: self.execute_cmd_head = "" """execute 指令的执行开头,用于被format""" - self.methods = MethodList( - [ - self._toCmdList_m1, - self._toCmdList_m2, - self._toCmdList_m3, - self._toCmdList_m4, - ] - ) - """转换算法列表,你觉得我为什么要这样调用函数?""" - - self.methods_byDelay = MethodList( - [ - self._toCmdList_withDelay_m1, - self._toCmdList_withDelay_m2, - self._toCmdList_withDelay_m3, - ] - ) - """转换算法列表,但是是对于延迟播放器的,你觉得我为什么要这样调用函数?""" - self.enable_old_exe_format = enable_old_exe_format """是否启用旧版指令格式""" @@ -194,7 +112,7 @@ class midiConvert: if enable_old_exe_format else "execute as {} at @s positioned ~ ~ ~ run " ) - """execute指令的应用,两个版本提前决定。""" + """execute指令头部""" def convert(self, midi_file: str, output_path: str): """转换前需要先运行此函数来获取基本信息""" @@ -215,7 +133,7 @@ class midiConvert: """文件名,不含路径且不含后缀""" @staticmethod - def __Inst2soundID_withX( + def inst_to_souldID_withX( instrumentID: int, ): """ @@ -239,12 +157,12 @@ class midiConvert: tuple(str我的世界乐器名, int转换算法中的X) """ try: - return pitched_instrument_list[instrumentID] + return PITCHED_INSTRUMENT_LIST[instrumentID] except KeyError: return "note.flute", 5 @staticmethod - def __bitInst2ID_withX(instrumentID: int): + def perc_inst_to_soundID_withX(instrumentID: int): """ 对于Midi第10通道所对应的打击乐器,返回我的世界乐器名 @@ -258,7 +176,7 @@ class midiConvert: tuple(str我的世界乐器名, int转换算法中的X) """ try: - return percussion_instrument_list[instrumentID] + return PERCUSSION_INSTRUMENT_LIST[instrumentID] except KeyError: print("WARN", f"无法使用打击乐器列表库,或者使用了不存在的乐器,打击乐器使用Dislink算法代替。{instrumentID}") if instrumentID == 55: @@ -270,14 +188,7 @@ class midiConvert: else: return "note.bd", 7 - @staticmethod - def score2time(score: int): - """ - 将《我的世界》的计分(以游戏刻计)转为表示时间的字符串 - """ - return str(int(int(score / 20) / 60)) + ":" + str(int(int(score / 20) % 60)) - - def __form_progress_bar( + def form_progress_bar( self, max_score: int, scoreboard_name: str, @@ -316,6 +227,7 @@ class midiConvert: | `_` | 用以表示进度条占位| """ perEach = max_score / pgs_style.count("_") + '''每个进度条代表的分值''' result = [] @@ -323,7 +235,7 @@ class midiConvert: pgs_style = pgs_style.replace(r"%^s", str(max_score)) if r"%^t" in pgs_style: - pgs_style = pgs_style.replace(r"%^t", self.score2time(max_score)) + pgs_style = pgs_style.replace(r"%^t", self.mctick2timestr(max_score)) sbn_pc = scoreboard_name[:2] if r"%%%" in pgs_style: @@ -465,251 +377,37 @@ class midiConvert: return result - def _toCmdList_m1( + def to_command_list( 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: - if self.debug_mode: - raise ZeroSpeedError("播放速度仅可为正实数") - speed = 1 - MaxVolume = 1 if MaxVolume > 1 else (0.001 if MaxVolume <= 0 else MaxVolume) - - commands = 0 - maxscore = 0 - - # 分轨的思路其实并不好,但这个算法就是这样 - # 所以我建议用第二个方法 _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: - try: - nowscore = round( - (ticks * tempo) - / ((self.midi.ticks_per_beat * float(speed)) * 50000) - ) - except NameError: - raise NotDefineTempoError("计算当前分数时出错 未定义参量 Tempo") - maxscore = max(maxscore, nowscore) - if msg.channel == 9: - soundID, _X = self.__bitInst2ID_withX(instrumentID) - else: - soundID, _X = self.__Inst2soundID_withX(instrumentID) - - 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_m2( - self, - scoreboard_name: str = "mscplay", - MaxVolume: float = 1.0, - speed: float = 1.0, - ) -> list: - """ - 使用神羽和金羿的转换思路,将midi转换为我的世界命令列表 - :param scoreboard_name: 我的世界的计分板名称 - :param MaxVolume: 音量,注意:这里的音量范围为(0,1],如果超出将被处理为正确值,其原理为在距离玩家 (1 / volume -1) 的地方播放音频 - :param speed: 速度,注意:这里的速度指的是播放倍率,其原理为在播放音频的时候,每个音符的播放时间除以 speed - :return: tuple(命令列表, 命令个数, 计分板最大值) - """ - - if speed == 0: - if self.debug_mode: - raise ZeroSpeedError("播放速度仅可为正实数") - speed = 1 - MaxVolume = 1 if MaxVolume > 1 else (0.001 if MaxVolume <= 0 else MaxVolume) - - # 一个midi中仅有16个通道 我们通过通道来识别而不是音轨 - channels = { - 0: [], - 1: [], - 2: [], - 3: [], - 4: [], - 5: [], - 6: [], - 7: [], - 8: [], - 9: [], - 10: [], - 11: [], - 12: [], - 13: [], - 14: [], - 15: [], - 16: [], - } - - microseconds = 0 - - # 我们来用通道统计音乐信息 - for msg in self.midi: - microseconds += msg.time * 1000 # 任何人都tm不要动这里,这里循环方式不是track,所以,这里的计时方式不一样 - if not msg.is_meta: - if self.debug_mode: - try: - if msg.channel > 15: - raise ChannelOverFlowError(f"当前消息 {msg} 的通道超限(≤15)") - except AttributeError: - pass - - if msg.type == "program_change": - channels[msg.channel].append(("PgmC", msg.program, microseconds)) - - elif msg.type == "note_on" and msg.velocity != 0: - channels[msg.channel].append( - ("NoteS", msg.note, msg.velocity, microseconds) - ) - - elif (msg.type == "note_on" and msg.velocity == 0) or ( - msg.type == "note_off" - ): - channels[msg.channel].append(("NoteE", msg.note, microseconds)) - - """整合后的音乐通道格式 - 每个通道包括若干消息元素其中逃不过这三种: - - 1 切换乐器消息 - ("PgmC", 切换后的乐器ID: int, 距离演奏开始的毫秒) - - 2 音符开始消息 - ("NoteS", 开始的音符ID, 力度(响度), 距离演奏开始的毫秒) - - 3 音符结束消息 - ("NoteS", 结束的音符ID, 距离演奏开始的毫秒)""" - - tracks = [] - cmdAmount = 0 - maxScore = 0 - - # 此处 我们把通道视为音轨 - for i in channels.keys(): - # 如果当前通道为空 则跳过 - if not channels[i]: - continue - - if i == 9: - SpecialBits = True - else: - SpecialBits = False - - nowTrack = [] - - for msg in channels[i]: - if msg[0] == "PgmC": - InstID = msg[1] - - elif msg[0] == "NoteS": - try: - soundID, _X = ( - self.__bitInst2ID_withX(InstID) - if SpecialBits - else self.__Inst2soundID_withX(InstID) - ) - except UnboundLocalError as E: - if self.debug_mode: - raise NotDefineProgramError(f"未定义乐器便提前演奏。\n{E}") - else: - soundID, _X = ( - self.__bitInst2ID_withX(-1) - if SpecialBits - else self.__Inst2soundID_withX(-1) - ) - 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_m3( - self, - scoreboard_name: str = "mscplay", - MaxVolume: float = 1.0, + max_volume: float = 1.0, speed: float = 1.0, ) -> list: """ 使用金羿的转换思路,将midi转换为我的世界命令列表 - :param scoreboard_name: 我的世界的计分板名称 - :param MaxVolume: 音量,注意:这里的音量范围为(0,1],如果超出将被处理为正确值,其原理为在距离玩家 (1 / volume -1) 的地方播放音频 - :param speed: 速度,注意:这里的速度指的是播放倍率,其原理为在播放音频的时候,每个音符的播放时间除以 speed - :return: tuple(命令列表, 命令个数, 计分板最大值) + + Parameters + ---------- + scoreboard_name: str + 我的世界的计分板名称 + max_volume: float + 最大播放音量,注意:这里的音量范围为(0,1],如果超出将被处理为正确值,其原理为在距离玩家 (1 / volume -1) 的地方播放 + speed: float + 速度,注意:这里的速度指的是播放倍率,其原理为在播放音频的时候,每个音符的播放时间除以 speed + + Returns + ------- + tuple( list[list[str指令,... ],... ], int指令数量, int最大计分 ) """ if speed == 0: if self.debug_mode: raise ZeroSpeedError("播放速度仅可为正实数") speed = 1 - MaxVolume = 1 if MaxVolume > 1 else (0.001 if MaxVolume <= 0 else MaxVolume) + max_volume = 1 if max_volume > 1 else (0.001 if max_volume <= 0 else max_volume) # 一个midi中仅有16个通道 我们通过通道来识别而不是音轨 - channels = { - 0: {}, - 1: {}, - 2: {}, - 3: {}, - 4: {}, - 5: {}, - 6: {}, - 7: {}, - 8: {}, - 9: {}, - 10: {}, - 11: {}, - 12: {}, - 13: {}, - 14: {}, - 15: {}, - 16: {}, - } + channels = empty_midi_channels() # 我们来用通道统计音乐信息 # 但是是用分轨的思路的 @@ -802,18 +500,18 @@ class midiConvert: elif msg[0] == "NoteS": try: soundID, _X = ( - self.__bitInst2ID_withX(InstID) + self.perc_inst_to_soundID_withX(InstID) if SpecialBits - else self.__Inst2soundID_withX(InstID) + else self.inst_to_souldID_withX(InstID) ) except UnboundLocalError as E: if self.debug_mode: raise NotDefineProgramError(f"未定义乐器便提前演奏。\n{E}") else: soundID, _X = ( - self.__bitInst2ID_withX(-1) + self.perc_inst_to_soundID_withX(-1) if SpecialBits - else self.__Inst2soundID_withX(-1) + else self.inst_to_souldID_withX(-1) ) score_now = round(msg[-1] / float(speed) / 50) maxScore = max(maxScore, score_now) @@ -824,7 +522,7 @@ class midiConvert: .replace("(", r"{") .replace(")", r"}") ) - + f"playsound {soundID} @s ^ ^ ^{1 / MaxVolume - 1} {msg[2] / 128} " + + f"playsound {soundID} @s ^ ^ ^{1 / max_volume - 1} {msg[2] / 128} " f"{2 ** ((msg[1] - 60 - _X) / 12)}" ) @@ -835,442 +533,23 @@ class midiConvert: return [tracks, cmdAmount, maxScore] - # 简单的单音填充 - def _toCmdList_m4( + + def to_command_list_with_delay( self, - scoreboard_name: str = "mscplay", - MaxVolume: float = 1.0, + max_volume: float = 1.0, speed: float = 1.0, - ) -> list: - """ - 使用金羿的转换思路,将midi转换为我的世界命令列表,并使用完全填充算法优化音感 - :param scoreboard_name: 我的世界的计分板名称 - :param MaxVolume: 音量,注意:这里的音量范围为(0,1],如果超出将被处理为正确值,其原理为在距离玩家 (1 / volume -1) 的地方播放音频 - :param speed: 速度,注意:这里的速度指的是播放倍率,其原理为在播放音频的时候,每个音符的播放时间除以 speed - :return: tuple(命令列表, 命令个数, 计分板最大值) - """ - # TODO: 这里的时间转换不知道有没有问题 - - if speed == 0: - if self.debug_mode: - raise ZeroSpeedError("播放速度仅可为正实数") - speed = 1 - MaxVolume = 1 if MaxVolume > 1 else (0.001 if MaxVolume <= 0 else MaxVolume) - - # 一个midi中仅有16个通道 我们通过通道来识别而不是音轨 - channels = [[], [], [], [], [], [], [], [], [], [], [], [], [], [], [], []] - - # 我们来用通道统计音乐信息 - for i, track in enumerate(self.midi.tracks): - microseconds = 0 - - for msg in track: - if msg.time != 0: - try: - microseconds += msg.time * tempo / self.midi.ticks_per_beat - except NameError: - raise NotDefineTempoError("计算当前分数时出错 未定义参量 Tempo") - - if msg.is_meta: - if msg.type == "set_tempo": - tempo = msg.tempo - else: - if self.debug_mode: - try: - if msg.channel > 15: - raise ChannelOverFlowError(f"当前消息 {msg} 的通道超限(≤15)") - except AttributeError: - pass - - if msg.type == "program_change": - channels[msg.channel].append( - ("PgmC", msg.program, microseconds) - ) - - elif msg.type == "note_on" and msg.velocity != 0: - channels[msg.channel].append( - ("NoteS", msg.note, msg.velocity, microseconds) - ) - - elif (msg.type == "note_on" and msg.velocity == 0) or ( - msg.type == "note_off" - ): - channels[msg.channel].append(("NoteE", msg.note, microseconds)) - - """整合后的音乐通道格式 - 每个通道包括若干消息元素其中逃不过这三种: - - 1 切换乐器消息 - - ("PgmC", 切换后的乐器ID: int, 距离演奏开始的毫秒) - - 2 音符开始消息 - - ("NoteS", 开始的音符ID, 力度(响度), 距离演奏开始的毫秒) - - 3 音符结束消息 - - ("NoteS", 结束的音符ID, 距离演奏开始的毫秒)""" - - note_channels = [[], [], [], [], [], [], [], [], [], [], [], [], [], [], [], []] - - # 此处 我们把通道视为音轨 - for i in range(len(channels)): - # 如果当前通道为空 则跳过 - - noteMsgs = [] - MsgIndex = [] - - for msg in channels[i]: - if msg[0] == "PgmC": - InstID = msg[1] - - elif msg[0] == "NoteS": - noteMsgs.append(msg[1:]) - MsgIndex.append(msg[1]) - - elif msg[0] == "NoteE": - if msg[1] in MsgIndex: - note_channels[i].append( - SingleNote( - InstID, - msg[1], - noteMsgs[MsgIndex.index(msg[1])][1], - noteMsgs[MsgIndex.index(msg[1])][2], - msg[-1] - noteMsgs[MsgIndex.index(msg[1])][2], - ) - ) - noteMsgs.pop(MsgIndex.index(msg[1])) - MsgIndex.pop(MsgIndex.index(msg[1])) - - tracks = [] - cmdAmount = 0 - maxScore = 0 - CheckFirstChannel = False - - # 临时用的插值计算函数 - def _linearFun(_note: SingleNote) -> list: - """传入音符数据,返回以半秒为分割的插值列表 - :param _note: SingleNote 音符 - :return list[tuple(int开始时间(毫秒), int乐器, int音符, int力度(内置), float音量(播放)),]""" - - result = [] - - totalCount = int(_note.lastTime / 500) - - for _i in range(totalCount): - result.append( - ( - _note.startTime + _i * 500, - _note.instrument, - _note.pitch, - _note.velocity, - MaxVolume * ((totalCount - _i) / totalCount), - ) - ) - - return result - - # 此处 我们把通道视为音轨 - for track in note_channels: - # 如果当前通道为空 则跳过 - if not track: - continue - - if note_channels.index(track) == 0: - CheckFirstChannel = True - SpecialBits = False - elif note_channels.index(track) == 9: - SpecialBits = True - else: - CheckFirstChannel = False - SpecialBits = False - - nowTrack = [] - - for note in track: - for every_note in _linearFun(note): - # 应该是计算的时候出了点小问题 - # 我们应该用一个MC帧作为时间单位而不是半秒 - - if SpecialBits: - soundID, _X = self.__bitInst2ID_withX(InstID) - else: - soundID, _X = self.__Inst2soundID_withX(InstID) - - score_now = round(every_note[0] / speed / 50000) - - maxScore = max(maxScore, score_now) - - nowTrack.append( - "execute @a[scores={" - + str(scoreboard_name) - + "=" - + str(score_now) - + "}" - + f"] ~ ~ ~ playsound {soundID} @s ~ ~{1 / every_note[4] - 1} ~ " - f"{note.velocity * (0.7 if CheckFirstChannel else 0.9)} {2 ** ((note.pitch - 60 - _X) / 12)}" - ) - - cmdAmount += 1 - 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: - if self.debug_mode: - raise ZeroSpeedError("播放速度仅可为正实数") - speed = 1 - - MaxVolume = 1 if MaxVolume > 1 else (0.001 if MaxVolume <= 0 else MaxVolume) - - 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) - ) - soundID, _X = self.__Inst2soundID_withX(instrumentID) - 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: - if self.debug_mode: - raise ZeroSpeedError("播放速度仅可为正实数") - speed = 1 - - MaxVolume = 1 if MaxVolume > 1 else (0.001 if MaxVolume <= 0 else MaxVolume) - - # 一个midi中仅有16个通道 我们通过通道来识别而不是音轨 - channels = { - 0: [], - 1: [], - 2: [], - 3: [], - 4: [], - 5: [], - 6: [], - 7: [], - 8: [], - 9: [], - 10: [], - 11: [], - 12: [], - 13: [], - 14: [], - 15: [], - 16: [], - } - - microseconds = 0 - - # 我们来用通道统计音乐信息 - for msg in self.midi: - try: - microseconds += ( - msg.time * 1000 - ) # 任何人都tm不要动这里,这里循环方式不是track,所以,这里的计时方式不一样 - - # print(microseconds) - except NameError: - if self.debug_mode: - raise NotDefineTempoError("计算当前分数时出错 未定义参量 Tempo") - else: - microseconds += ( - msg.time * 1000 - ) # 任何人都tm不要动这里,这里循环方式不是track,所以,这里的计时方式不一样 - - if msg.is_meta: - if msg.type == "set_tempo": - tempo = msg.tempo - else: - if self.debug_mode: - try: - if msg.channel > 15: - raise ChannelOverFlowError(f"当前消息 {msg} 的通道超限(≤15)") - except AttributeError: - pass - - if msg.type == "program_change": - channels[msg.channel].append(("PgmC", msg.program, microseconds)) - - elif msg.type == "note_on" and msg.velocity != 0: - channels[msg.channel].append( - ("NoteS", msg.note, msg.velocity, microseconds) - ) - - elif (msg.type == "note_on" and msg.velocity == 0) or ( - msg.type == "note_off" - ): - channels[msg.channel].append(("NoteE", msg.note, microseconds)) - - """整合后的音乐通道格式 - 每个通道包括若干消息元素其中逃不过这三种: - - 1 切换乐器消息 - ("PgmC", 切换后的乐器ID: int, 距离演奏开始的毫秒) - - 2 音符开始消息 - ("NoteS", 开始的音符ID, 力度(响度), 距离演奏开始的毫秒) - - 3 音符结束消息 - ("NoteS", 结束的音符ID, 距离演奏开始的毫秒)""" - - results = [] - - for i in channels.keys(): - # 如果当前通道为空 则跳过 - if not channels[i]: - continue - - if i == 9: - SpecialBits = True - else: - SpecialBits = False - - for msg in channels[i]: - if msg[0] == "PgmC": - InstID = msg[1] - - elif msg[0] == "NoteS": - try: - soundID, _X = ( - self.__bitInst2ID_withX(InstID) - if SpecialBits - else self.__Inst2soundID_withX(InstID) - ) - except UnboundLocalError as E: - if self.debug_mode: - raise NotDefineProgramError(f"未定义乐器便提前演奏。\n{E}") - else: - soundID, _X = ( - self.__bitInst2ID_withX(-1) - if SpecialBits - else self.__Inst2soundID_withX(-1) - ) - 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)] - - def _toCmdList_withDelay_m3( - self, - MaxVolume: float = 1.0, - speed: float = 1.0, - player: str = "@a", + player_selector: str = "@a", ) -> list: """ 使用金羿的转换思路,将midi转换为我的世界命令列表,并输出每个音符之后的延迟 Parameters ---------- - MaxVolume: float + max_volume: float 最大播放音量,注意:这里的音量范围为(0,1],如果超出将被处理为正确值,其原理为在距离玩家 (1 / volume -1) 的地方播放音频 speed: float 速度,注意:这里的速度指的是播放倍率,其原理为在播放音频的时候,每个音符的播放时间除以 speed - player: str + player_selector: str 玩家选择器,默认为`@a` Returns @@ -1282,28 +561,10 @@ class midiConvert: if self.debug_mode: raise ZeroSpeedError("播放速度仅可为正实数") speed = 1 - MaxVolume = 1 if MaxVolume > 1 else (0.001 if MaxVolume <= 0 else MaxVolume) + max_volume = 1 if max_volume > 1 else (0.001 if max_volume <= 0 else max_volume) # 一个midi中仅有16个通道 我们通过通道来识别而不是音轨 - channels = { - 0: {}, - 1: {}, - 2: {}, - 3: {}, - 4: {}, - 5: {}, - 6: {}, - 7: {}, - 8: {}, - 9: {}, - 10: {}, - 11: {}, - 12: {}, - 13: {}, - 14: {}, - 15: {}, - 16: {}, - } + channels = empty_midi_channels() # 我们来用通道统计音乐信息 # 但是是用分轨的思路的 @@ -1391,32 +652,32 @@ class midiConvert: elif msg[0] == "NoteS": try: soundID, _X = ( - self.__bitInst2ID_withX(InstID) + self.perc_inst_to_soundID_withX(InstID) if SpecialBits - else self.__Inst2soundID_withX(InstID) + else self.inst_to_souldID_withX(InstID) ) except UnboundLocalError as E: if self.debug_mode: raise NotDefineProgramError(f"未定义乐器便提前演奏。\n{E}") else: soundID, _X = ( - self.__bitInst2ID_withX(-1) + self.perc_inst_to_soundID_withX(-1) if SpecialBits - else self.__Inst2soundID_withX(-1) + else self.inst_to_souldID_withX(-1) ) score_now = round(msg[-1] / float(speed) / 50) # print(score_now) try: tracks[score_now].append( - self.execute_cmd_head.format(player) - + f"playsound {soundID} @s ^ ^ ^{1 / MaxVolume - 1} {msg[2] / 128} " + self.execute_cmd_head.format(player_selector) + + f"playsound {soundID} @s ^ ^ ^{1 / max_volume - 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} " + self.execute_cmd_head.format(player_selector) + + f"playsound {soundID} @s ^ ^ ^{1 / max_volume - 1} {msg[2] / 128} " f"{2 ** ((msg[1] - 60 - _X) / 12)}" ] @@ -1443,32 +704,37 @@ class midiConvert: return [results, max(all_ticks)] + def to_mcpack( self, - method: int = 1, volume: float = 1.0, speed: float = 1.0, - progressbar: Union[bool, tuple] = None, + progressbar: Union[bool, Tuple[str, Tuple[str,]]] = None, scoreboard_name: str = "mscplay", - isAutoReset: bool = False, + auto_reset: bool = False, ) -> tuple: """ - 使用method指定的转换算法,将midi转换为我的世界mcpack格式的包 - :param method: 转换算法 - :param isAutoReset: 是否自动重置计分板 - :param progressbar: 进度条,(当此参数为True时使用默认进度条,为其他的值为真的参数时识别为进度条自定义参数,为其他值为假的时候不生成进度条) - :param scoreboard_name: 我的世界的计分板名称 - :param volume: 音量,注意:这里的音量范围为(0,1],其原理为在距离玩家 (1 / volume -1) 的地方播放音频 - :param speed: 速度,注意:这里的速度指的是播放倍率,其原理为在播放音频的时候,每个音符的播放时间除以 speed - :return 成功与否,成功返回(True,True),失败返回(False,str失败原因) + 将midi转换为我的世界mcpack格式的包 + + Parameters + ---------- + volume: float + 音量,注意:这里的音量范围为(0,1],其原理为在距离玩家 (1 / volume -1) 的地方播放音频 + speed: float + 速度,注意:这里的速度指的是播放倍率,其原理为在播放音频的时候,每个音符的播放时间除以 speed + progressbar: bool|tuple[str, Tuple[str,]] + 进度条,当此参数为 `True` 时使用默认进度条,为其他的**值为真**的参数时识别为进度条自定义参数,为其他**值为假**的时候不生成进度条 + scoreboard_name: str + 我的世界的计分板名称 + auto_reset: bool + 是否自动重置计分板 + + Returns + ------- + tuple(int指令长度, int最大计分) """ - # try: - cmdlist, maxlen, maxscore = self.methods[method - 1]( - scoreboard_name, volume, speed - ) - # except: - # return (False, f"无法找到算法ID{method}对应的转换算法") + cmdlist, maxlen, maxscore = self.to_command_list(scoreboard_name, volume, speed) # 当文件f夹{self.outputPath}/temp/functions存在时清空其下所有项目,然后创建 if os.path.exists(f"{self.output_path}/temp/functions/"): @@ -1539,7 +805,7 @@ class midiConvert: + "..}]" + f" {scoreboard_name}\n" ) - if isAutoReset + if auto_reset else "", f"function mscplay/progressShow\n" if progressbar else "", ) @@ -1555,7 +821,7 @@ class midiConvert: encoding="utf-8", ) as f: f.writelines( - "\n".join(self.__form_progress_bar(maxscore, scoreboard_name)) + "\n".join(self.form_progress_bar(maxscore, scoreboard_name)) ) else: with open( @@ -1565,7 +831,7 @@ class midiConvert: ) as f: f.writelines( "\n".join( - self.__form_progress_bar( + self.form_progress_bar( maxscore, scoreboard_name, progressbar ) ) @@ -1582,7 +848,7 @@ class midiConvert: shutil.rmtree(f"{self.output_path}/temp/") - return True, maxlen, maxscore + return maxlen, maxscore def to_mcpack_with_delay( self, @@ -1711,7 +977,7 @@ class midiConvert: pgb_struct, pgbSize, pgbNowPos = commands_to_structure( [ (i, 0) - for i in self.__form_progress_bar(max_delay, scb_name, progressbar) + for i in self.form_progress_bar(max_delay, scb_name, progressbar) ], max_height - 1, ) @@ -1898,11 +1164,11 @@ class midiConvert: [ (i, 0) for i in ( - self.__form_progress_bar(maxScore, scoreboard_name) + self.form_progress_bar(maxScore, scoreboard_name) # 此处是对于仅有 True 的参数和自定义参数的判断 # 改这一行没🐎 if progressbar is True - else self.__form_progress_bar( + else self.form_progress_bar( maxScore, scoreboard_name, progressbar ) ) @@ -2003,7 +1269,7 @@ class midiConvert: pgbBytes, pgbSize, pgbNowPos = commands_to_BDX_bytes( [ (i, 0) - for i in self.__form_progress_bar(max_delay, scb_name, progressbar) + for i in self.form_progress_bar(max_delay, scb_name, progressbar) ], max_height - 1, ) diff --git a/Musicreater/plugin/__init__.py b/Musicreater/plugin/__init__.py new file mode 100644 index 0000000..a07c08d --- /dev/null +++ b/Musicreater/plugin/__init__.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +""" +存放非音·创本体的附加内容(插件?) + +版权所有 © 2023 音·创 开发者 +Copyright © 2023 all the developers of Musicreater + +开源相关声明请见 ../../License.md +Terms & Conditions: ../../License.md +""" + +# 睿穆组织 开发交流群 861684859 +# Email TriM-Organization@hotmail.com +# 版权所有 金羿("Eilles Wan") & 诸葛亮与八卦阵("bgArray") & 鸣凤鸽子("MingFengPigeon") +# 若需转载或借鉴 许可声明请查看仓库目录下的 License.md + + +__all__ = [] +__author__ = (("金羿", "Eilles Wan"), ("诸葛亮与八卦阵", "bgArray"), ("鸣凤鸽子", "MingFengPigeon")) \ No newline at end of file diff --git a/Musicreater/plugin/archive.py b/Musicreater/plugin/archive.py new file mode 100644 index 0000000..253b0f2 --- /dev/null +++ b/Musicreater/plugin/archive.py @@ -0,0 +1,26 @@ + + + +import os +import zipfile + + +def compress_zipfile(sourceDir, outFilename, compression=8, exceptFile=None): + """使用compression指定的算法打包目录为zip文件\n + 默认算法为DEFLATED(8),可用算法如下:\n + STORED = 0\n + DEFLATED = 8\n + BZIP2 = 12\n + LZMA = 14\n + """ + + zipf = zipfile.ZipFile(outFilename, "w", compression) + pre_len = len(os.path.dirname(sourceDir)) + for parent, dirnames, filenames in os.walk(sourceDir): + for filename in filenames: + if filename == exceptFile: + continue + pathfile = os.path.join(parent, filename) + arc_name = pathfile[pre_len:].strip(os.path.sep) # 相对路径 + zipf.write(pathfile, arc_name) + zipf.close() \ No newline at end of file diff --git a/Musicreater/plugin/bdx.py b/Musicreater/plugin/bdx.py new file mode 100644 index 0000000..a9d8ffd --- /dev/null +++ b/Musicreater/plugin/bdx.py @@ -0,0 +1,199 @@ +''' +存放有关BDX结构操作的内容 +''' + + +from .common import bottem_side_length_of_smallest_square_bottom_box +from ..constants import x,y,z + + +bdx_key = { + "x": [b"\x0f", b"\x0e", b"\x1c", b"\x14", b"\x15"], + "y": [b"\x11", b"\x10", b"\x1d", b"\x16", b"\x17"], + "z": [b"\x13", b"\x12", b"\x1e", b"\x18", b"\x19"], +} +"""key存储了方块移动指令的数据,其中可以用key[x|y|z][0|1]来表示xyz的减或增 +而key[][2+]是用来增加指定数目的""" + + +def bdx_move(axis: str, value: int): + if value == 0: + return b"" + if abs(value) == 1: + return bdx_key[axis][0 if value == -1 else 1] + + pointer = sum( + [ + 1 if i else 0 + for i in ( + value != -1, + value < -1 or value > 1, + value < -128 or value > 127, + value < -32768 or value > 32767, + ) + ] + ) + + return bdx_key[axis][pointer] + value.to_bytes( + 2 ** (pointer - 2), "big", signed=True + ) + + + +def form_command_block_in_BDX_bytes( + command: str, + particularValue: int, + impluse: int = 0, + condition: bool = False, + needRedstone: bool = True, + tickDelay: int = 0, + customName: str = "", + executeOnFirstTick: bool = False, + trackOutput: bool = True, +): + """ + 使用指定项目返回指定的指令方块放置指令项 + :param command: `str` + 指令 + :param particularValue: + 方块特殊值,即朝向 + :0 下 无条件 + :1 上 无条件 + :2 z轴负方向 无条件 + :3 z轴正方向 无条件 + :4 x轴负方向 无条件 + :5 x轴正方向 无条件 + :6 下 无条件 + :7 下 无条件 + + :8 下 有条件 + :9 上 有条件 + :10 z轴负方向 有条件 + :11 z轴正方向 有条件 + :12 x轴负方向 有条件 + :13 x轴正方向 有条件 + :14 下 有条件 + :14 下 有条件 + 注意!此处特殊值中的条件会被下面condition参数覆写 + :param impluse: `int 0|1|2` + 方块类型 + 0脉冲 1循环 2连锁 + :param condition: `bool` + 是否有条件 + :param needRedstone: `bool` + 是否需要红石 + :param tickDelay: `int` + 执行延时 + :param customName: `str` + 悬浮字 + lastOutput: `str` + 上次输出字符串,注意此处需要留空 + :param executeOnFirstTick: `bool` + 首刻执行(循环指令方块是否激活后立即执行,若为False,则从激活时起延迟后第一次执行) + :param trackOutput: `bool` + 是否输出 + + :return:str + """ + block = b"\x24" + particularValue.to_bytes(2, byteorder="big", signed=False) + + for i in [ + impluse.to_bytes(4, byteorder="big", signed=False), + bytes(command, encoding="utf-8") + b"\x00", + bytes(customName, encoding="utf-8") + b"\x00", + bytes("", encoding="utf-8") + b"\x00", + tickDelay.to_bytes(4, byteorder="big", signed=True), + executeOnFirstTick.to_bytes(1, byteorder="big"), + trackOutput.to_bytes(1, byteorder="big"), + condition.to_bytes(1, byteorder="big"), + needRedstone.to_bytes(1, byteorder="big"), + ]: + block += i + return block + + +def commands_to_BDX_bytes( + commands: list, + max_height: int = 64, +): + """ + :param commands: 指令列表(指令, 延迟) + :param max_height: 生成结构最大高度 + :return 成功与否,成功返回(True,未经过压缩的源,结构占用大小),失败返回(False,str失败原因) + """ + + _sideLength = bottem_side_length_of_smallest_square_bottom_box( + len(commands), max_height + ) + _bytes = b"" + + y_forward = True + z_forward = True + + now_y = 0 + now_z = 0 + now_x = 0 + + for cmd, delay in commands: + impluse = 2 + condition = False + needRedstone = False + tickDelay = delay + customName = "" + executeOnFirstTick = False + trackOutput = True + _bytes += form_command_block_in_BDX_bytes( + cmd, + (1 if y_forward else 0) + if ( + ((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, + impluse=impluse, + condition=condition, + needRedstone=needRedstone, + tickDelay=tickDelay, + customName=customName, + executeOnFirstTick=executeOnFirstTick, + trackOutput=trackOutput, + ) + + now_y += 1 if y_forward else -1 + + if ((now_y >= max_height) and y_forward) or ((now_y < 0) and (not y_forward)): + now_y -= 1 if y_forward else -1 + + y_forward = not y_forward + + now_z += 1 if z_forward else -1 + + if ((now_z >= _sideLength) and z_forward) or ( + (now_z < 0) and (not z_forward) + ): + now_z -= 1 if z_forward else -1 + z_forward = not z_forward + _bytes += bdx_key[x][1] + now_x += 1 + else: + _bytes += bdx_key[z][int(z_forward)] + + else: + _bytes += bdx_key[y][int(y_forward)] + + return ( + _bytes, + [ + now_x + 1, + max_height if now_x or now_z else now_y, + _sideLength if now_x else now_z, + ], + [now_x, now_y, now_z], + ) + + diff --git a/Musicreater/plugin/common.py b/Musicreater/plugin/common.py new file mode 100644 index 0000000..94684c0 --- /dev/null +++ b/Musicreater/plugin/common.py @@ -0,0 +1,14 @@ +''' +存放通用的普遍性的内容 +''' + +import math + + +def bottem_side_length_of_smallest_square_bottom_box(total: int, maxHeight: int): + """给定总方块数量和最大高度,返回所构成的图形外切正方形的边长 + :param total: 总方块数量 + :param maxHeight: 最大高度 + :return: 外切正方形的边长 int""" + return math.ceil(math.sqrt(math.ceil(total / maxHeight))) + diff --git a/Musicreater/plugin/mcstructure.py b/Musicreater/plugin/mcstructure.py new file mode 100644 index 0000000..33e13cb --- /dev/null +++ b/Musicreater/plugin/mcstructure.py @@ -0,0 +1,237 @@ +''' +存放有关MCSTRUCTURE结构操作的内容 +''' + +from .common import bottem_side_length_of_smallest_square_bottom_box +from TrimMCStruct import Structure, Block, TAG_Long, TAG_Byte + +def form_note_block_in_NBT_struct( + note: int, coordinate: tuple, instrument: str = "note.harp", powered: bool = False +): + """生成音符盒方块 + :param note: `int`(0~24) + 音符的音高 + :param coordinate: `tuple[int,int,int]` + 此方块所在之相对坐标 + :param instrument: `str` + 音符盒的乐器 + :param powered: `bool` + 是否已被激活 + :return Block + """ + + return Block( + "minecraft", + "noteblock", + { + "instrument": instrument.replace("note.", ""), + "note": note, + "powered": powered, + }, + { + "block_entity_data": { + "note": TAG_Byte(note), + "id": "noteblock", + "x": coordinate[0], + "y": coordinate[1], + "z": coordinate[2], + } + }, + ) + + +def form_repeater_in_NBT_struct( + delay: int, facing: int +): + """生成中继器方块 + :param facing: + :param delay: 1~4 + :return Block()""" + + + return Block( + "minecraft", + "unpowered_repeater", + { + "repeater_delay": delay, + "direction": facing, + }, + ) + + +def form_command_block_in_NBT_struct( + command: str, + coordinate: tuple, + particularValue: int, + impluse: int = 0, + condition: bool = False, + alwaysRun: bool = True, + tickDelay: int = 0, + customName: str = "", + executeOnFirstTick: bool = False, + trackOutput: bool = True, +): + """ + 使用指定项目返回指定的指令方块结构 + :param command: `str` + 指令 + :param coordinate: `tuple[int,int,int]` + 此方块所在之相对坐标 + :param particularValue: + 方块特殊值,即朝向 + :0 下 无条件 + :1 上 无条件 + :2 z轴负方向 无条件 + :3 z轴正方向 无条件 + :4 x轴负方向 无条件 + :5 x轴正方向 无条件 + :6 下 无条件 + :7 下 无条件 + + :8 下 有条件 + :9 上 有条件 + :10 z轴负方向 有条件 + :11 z轴正方向 有条件 + :12 x轴负方向 有条件 + :13 x轴正方向 有条件 + :14 下 有条件 + :14 下 有条件 + 注意!此处特殊值中的条件会被下面condition参数覆写 + :param impluse: `int 0|1|2` + 方块类型 + 0脉冲 1循环 2连锁 + :param condition: `bool` + 是否有条件 + :param alwaysRun: `bool` + 是否始终执行 + :param tickDelay: `int` + 执行延时 + :param customName: `str` + 悬浮字 + :param executeOnFirstTick: `bool` + 首刻执行(循环指令方块是否激活后立即执行,若为False,则从激活时起延迟后第一次执行) + :param trackOutput: `bool` + 是否输出 + + :return:str + """ + + + return Block( + "minecraft", + "command_block" + if impluse == 0 + else ("repeating_command_block" if impluse == 1 else "chain_command_block"), + states={"conditional_bit": condition, "facing_direction": particularValue}, + extra_data={ + "block_entity_data": { + "Command": command, + "CustomName": customName, + "ExecuteOnFirstTick": executeOnFirstTick, + "LPCommandMode": 0, + "LPCondionalMode": False, + "LPRedstoneMode": False, + "LastExecution": TAG_Long(0), + "LastOutput": "", + "LastOutputParams": [], + "SuccessCount": 0, + "TickDelay": tickDelay, + "TrackOutput": trackOutput, + "Version": 25, + "auto": alwaysRun, + "conditionMet": False, # 是否已经满足条件 + "conditionalMode": condition, + "id": "CommandBlock", + "isMovable": True, + "powered": False, # 是否已激活 + "x": coordinate[0], + "y": coordinate[1], + "z": coordinate[2], + } + }, + compability_version=17959425, + ) + + +def commands_to_structure( + commands: list, + max_height: int = 64, +): + """ + :param commands: 指令列表(指令, 延迟) + :param max_height: 生成结构最大高度 + :return 成功与否,成功返回(结构类,结构占用大小),失败返回(False,str失败原因) + """ + + + _sideLength = bottem_side_length_of_smallest_square_bottom_box( + len(commands), max_height + ) + + struct = Structure( + (_sideLength, max_height, _sideLength), # 声明结构大小 + ) + + y_forward = True + z_forward = True + + now_y = 0 + now_z = 0 + now_x = 0 + + for cmd, delay in commands: + coordinate = (now_x, now_y, now_z) + struct.set_block( + coordinate, + form_command_block_in_NBT_struct( + command=cmd, + coordinate=coordinate, + particularValue=(1 if y_forward else 0) + if ( + ((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 + ), + impluse=2, + condition=False, + alwaysRun=True, + tickDelay=delay, + customName="", + executeOnFirstTick=False, + trackOutput=True, + ), + ) + + now_y += 1 if y_forward else -1 + + if ((now_y >= max_height) and y_forward) or ((now_y < 0) and (not y_forward)): + now_y -= 1 if y_forward else -1 + + y_forward = not y_forward + + now_z += 1 if z_forward else -1 + + if ((now_z >= _sideLength) and z_forward) or ( + (now_z < 0) and (not z_forward) + ): + now_z -= 1 if z_forward else -1 + z_forward = not z_forward + now_x += 1 + + return ( + struct, + ( + now_x + 1, + max_height if now_x or now_z else now_y, + _sideLength if now_x else now_z, + ), + (now_x, now_y, now_z), + ) + diff --git a/Musicreater/previous.py b/Musicreater/previous.py new file mode 100644 index 0000000..40e4c9c --- /dev/null +++ b/Musicreater/previous.py @@ -0,0 +1,449 @@ +''' +旧版本功能以及已经弃用的函数 +''' + +from .exceptions import * +from .main import midiConvert + + +def to_command_list_method1( + self: midiConvert, + 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: + if self.debug_mode: + raise ZeroSpeedError("播放速度仅可为正实数") + speed = 1 + MaxVolume = 1 if MaxVolume > 1 else (0.001 if MaxVolume <= 0 else MaxVolume) + + commands = 0 + maxscore = 0 + + # 分轨的思路其实并不好,但这个算法就是这样 + # 所以我建议用第二个方法 _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: + try: + nowscore = round( + (ticks * tempo) + / ((self.midi.ticks_per_beat * float(speed)) * 50000) + ) + except NameError: + raise NotDefineTempoError("计算当前分数时出错 未定义参量 Tempo") + maxscore = max(maxscore, nowscore) + if msg.channel == 9: + soundID, _X = self.perc_inst_to_soundID_withX(instrumentID) + else: + soundID, _X = self.inst_to_souldID_withX(instrumentID) + + 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_m2( + self: midiConvert, + scoreboard_name: str = "mscplay", + MaxVolume: float = 1.0, + speed: float = 1.0, +) -> list: + """ + 使用神羽和金羿的转换思路,将midi转换为我的世界命令列表 + :param scoreboard_name: 我的世界的计分板名称 + :param MaxVolume: 音量,注意:这里的音量范围为(0,1],如果超出将被处理为正确值,其原理为在距离玩家 (1 / volume -1) 的地方播放音频 + :param speed: 速度,注意:这里的速度指的是播放倍率,其原理为在播放音频的时候,每个音符的播放时间除以 speed + :return: tuple(命令列表, 命令个数, 计分板最大值) + """ + + if speed == 0: + if self.debug_mode: + raise ZeroSpeedError("播放速度仅可为正实数") + speed = 1 + MaxVolume = 1 if MaxVolume > 1 else (0.001 if MaxVolume <= 0 else MaxVolume) + + # 一个midi中仅有16个通道 我们通过通道来识别而不是音轨 + channels = { + 0: [], + 1: [], + 2: [], + 3: [], + 4: [], + 5: [], + 6: [], + 7: [], + 8: [], + 9: [], + 10: [], + 11: [], + 12: [], + 13: [], + 14: [], + 15: [], + 16: [], + } + + microseconds = 0 + + # 我们来用通道统计音乐信息 + for msg in self.midi: + microseconds += msg.time * 1000 # 任何人都tm不要动这里,这里循环方式不是track,所以,这里的计时方式不一样 + if not msg.is_meta: + if self.debug_mode: + try: + if msg.channel > 15: + raise ChannelOverFlowError(f"当前消息 {msg} 的通道超限(≤15)") + except AttributeError: + pass + + if msg.type == "program_change": + channels[msg.channel].append(("PgmC", msg.program, microseconds)) + + elif msg.type == "note_on" and msg.velocity != 0: + channels[msg.channel].append( + ("NoteS", msg.note, msg.velocity, microseconds) + ) + + elif (msg.type == "note_on" and msg.velocity == 0) or ( + msg.type == "note_off" + ): + channels[msg.channel].append(("NoteE", msg.note, microseconds)) + + """整合后的音乐通道格式 + 每个通道包括若干消息元素其中逃不过这三种: + + 1 切换乐器消息 + ("PgmC", 切换后的乐器ID: int, 距离演奏开始的毫秒) + + 2 音符开始消息 + ("NoteS", 开始的音符ID, 力度(响度), 距离演奏开始的毫秒) + + 3 音符结束消息 + ("NoteS", 结束的音符ID, 距离演奏开始的毫秒)""" + + tracks = [] + cmdAmount = 0 + maxScore = 0 + + # 此处 我们把通道视为音轨 + for i in channels.keys(): + # 如果当前通道为空 则跳过 + if not channels[i]: + continue + + if i == 9: + SpecialBits = True + else: + SpecialBits = False + + nowTrack = [] + + for msg in channels[i]: + if msg[0] == "PgmC": + InstID = msg[1] + + elif msg[0] == "NoteS": + try: + soundID, _X = ( + self.perc_inst_to_soundID_withX(InstID) + if SpecialBits + else self.inst_to_souldID_withX(InstID) + ) + except UnboundLocalError as E: + if self.debug_mode: + raise NotDefineProgramError(f"未定义乐器便提前演奏。\n{E}") + else: + soundID, _X = ( + self.perc_inst_to_soundID_withX(-1) + if SpecialBits + else self.inst_to_souldID_withX(-1) + ) + 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: midiConvert, + 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: + if self.debug_mode: + raise ZeroSpeedError("播放速度仅可为正实数") + speed = 1 + + MaxVolume = 1 if MaxVolume > 1 else (0.001 if MaxVolume <= 0 else MaxVolume) + + 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) + ) + soundID, _X = self.inst_to_souldID_withX(instrumentID) + 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: midiConvert, + 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: + if self.debug_mode: + raise ZeroSpeedError("播放速度仅可为正实数") + speed = 1 + + MaxVolume = 1 if MaxVolume > 1 else (0.001 if MaxVolume <= 0 else MaxVolume) + + # 一个midi中仅有16个通道 我们通过通道来识别而不是音轨 + channels = { + 0: [], + 1: [], + 2: [], + 3: [], + 4: [], + 5: [], + 6: [], + 7: [], + 8: [], + 9: [], + 10: [], + 11: [], + 12: [], + 13: [], + 14: [], + 15: [], + 16: [], + } + + microseconds = 0 + + # 我们来用通道统计音乐信息 + for msg in self.midi: + try: + microseconds += msg.time * 1000 # 任何人都tm不要动这里,这里循环方式不是track,所以,这里的计时方式不一样 + + # print(microseconds) + except NameError: + if self.debug_mode: + raise NotDefineTempoError("计算当前分数时出错 未定义参量 Tempo") + else: + microseconds += ( + msg.time * 1000 + ) # 任何人都tm不要动这里,这里循环方式不是track,所以,这里的计时方式不一样 + + if msg.is_meta: + if msg.type == "set_tempo": + tempo = msg.tempo + else: + if self.debug_mode: + try: + if msg.channel > 15: + raise ChannelOverFlowError(f"当前消息 {msg} 的通道超限(≤15)") + except AttributeError: + pass + + if msg.type == "program_change": + channels[msg.channel].append(("PgmC", msg.program, microseconds)) + + elif msg.type == "note_on" and msg.velocity != 0: + channels[msg.channel].append( + ("NoteS", msg.note, msg.velocity, microseconds) + ) + + elif (msg.type == "note_on" and msg.velocity == 0) or ( + msg.type == "note_off" + ): + channels[msg.channel].append(("NoteE", msg.note, microseconds)) + + """整合后的音乐通道格式 + 每个通道包括若干消息元素其中逃不过这三种: + + 1 切换乐器消息 + ("PgmC", 切换后的乐器ID: int, 距离演奏开始的毫秒) + + 2 音符开始消息 + ("NoteS", 开始的音符ID, 力度(响度), 距离演奏开始的毫秒) + + 3 音符结束消息 + ("NoteS", 结束的音符ID, 距离演奏开始的毫秒)""" + + results = [] + + for i in channels.keys(): + # 如果当前通道为空 则跳过 + if not channels[i]: + continue + + if i == 9: + SpecialBits = True + else: + SpecialBits = False + + for msg in channels[i]: + if msg[0] == "PgmC": + InstID = msg[1] + + elif msg[0] == "NoteS": + try: + soundID, _X = ( + self.perc_inst_to_soundID_withX(InstID) + if SpecialBits + else self.inst_to_souldID_withX(InstID) + ) + except UnboundLocalError as E: + if self.debug_mode: + raise NotDefineProgramError(f"未定义乐器便提前演奏。\n{E}") + else: + soundID, _X = ( + self.perc_inst_to_soundID_withX(-1) + if SpecialBits + else self.inst_to_souldID_withX(-1) + ) + 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 new file mode 100644 index 0000000..6e29e63 --- /dev/null +++ b/Musicreater/subclass.py @@ -0,0 +1,91 @@ +''' +存储许多非主要的相关类 +''' + + +from dataclasses import dataclass +from typing import TypeVar + +T = TypeVar("T") # Declare type variable + + +@dataclass(init=False) +class SingleNote: + instrument: int + """乐器编号""" + note: int + """音符编号""" + velocity: int + """力度/响度""" + startTime: int + """开始之时 ms""" + lastTime: int + """音符持续时间 ms""" + + def __init__( + self, instrument: int, pitch: int, velocity: int, startTime: int, lastTime: int + ): + """用于存储单个音符的类 + :param instrument 乐器编号 + :param pitch 音符编号 + :param velocity 力度/响度 + :param startTime 开始之时(ms) + 注:此处的时间是用从乐曲开始到当前的毫秒数 + :param lastTime 音符延续时间(ms)""" + self.instrument: int = instrument + """乐器编号""" + self.note: int = pitch + """音符编号""" + self.velocity: int = velocity + """力度/响度""" + self.startTime: int = startTime + """开始之时 ms""" + self.lastTime: int = lastTime + """音符持续时间 ms""" + + @property + def inst(self): + """乐器编号""" + return self.instrument + + @inst.setter + def inst(self, inst_): + self.instrument = inst_ + + @property + def pitch(self): + """音符编号""" + return self.note + + def __str__(self): + return ( + f"Note(inst = {self.inst}, pitch = {self.note}, velocity = {self.velocity}, " + f"startTime = {self.startTime}, lastTime = {self.lastTime}, )" + ) + + def __tuple__(self): + return self.inst, self.note, self.velocity, self.startTime, self.lastTime + + def __dict__(self): + return { + "inst": self.inst, + "pitch": self.note, + "velocity": self.velocity, + "startTime": self.startTime, + "lastTime": self.lastTime, + } + + +class MethodList(list): + """函数列表,列表中的所有元素均为函数""" + + def __init__(self, in_=()): + """函数列表,列表中的所有元素均为函数""" + super().__init__() + self._T = [_x for _x in in_] + + def __getitem__(self, item) -> T: + return self._T[item] + + def __len__(self) -> int: + return self._T.__len__() diff --git a/Musicreater/utils.py b/Musicreater/utils.py index 5cb136a..d5b0971 100644 --- a/Musicreater/utils.py +++ b/Musicreater/utils.py @@ -1,459 +1,17 @@ -import math -import os - -bdx_key = { - "x": [b"\x0f", b"\x0e", b"\x1c", b"\x14", b"\x15"], - "y": [b"\x11", b"\x10", b"\x1d", b"\x16", b"\x17"], - "z": [b"\x13", b"\x12", b"\x1e", b"\x18", b"\x19"], -} -"""key存储了方块移动指令的数据,其中可以用key[x|y|z][0|1]来表示xyz的减或增 -而key[][2+]是用来增加指定数目的""" - -x = "x" -y = "y" -z = "z" +''' +存放主程序所必须的功能性内容 +''' -def bdx_move(axis: str, value: int): - if value == 0: - return b"" - if abs(value) == 1: - return bdx_key[axis][0 if value == -1 else 1] - - pointer = sum( - [ - 1 if i else 0 - for i in ( - value != -1, - value < -1 or value > 1, - value < -128 or value > 127, - value < -32768 or value > 32767, - ) - ] - ) - - return bdx_key[axis][pointer] + value.to_bytes( - 2 ** (pointer - 2), "big", signed=True - ) - - -def compress_zipfile(sourceDir, outFilename, compression=8, exceptFile=None): - """使用compression指定的算法打包目录为zip文件\n - 默认算法为DEFLATED(8),可用算法如下:\n - STORED = 0\n - DEFLATED = 8\n - BZIP2 = 12\n - LZMA = 14\n +def mctick2timestr(mc_tick: int): """ - import zipfile - - zipf = zipfile.ZipFile(outFilename, "w", compression) - pre_len = len(os.path.dirname(sourceDir)) - for parent, dirnames, filenames in os.walk(sourceDir): - for filename in filenames: - if filename == exceptFile: - continue - pathfile = os.path.join(parent, filename) - arc_name = pathfile[pre_len:].strip(os.path.sep) # 相对路径 - zipf.write(pathfile, arc_name) - zipf.close() - - -def form_command_block_in_BDX_bytes( - command: str, - particularValue: int, - impluse: int = 0, - condition: bool = False, - needRedstone: bool = True, - tickDelay: int = 0, - customName: str = "", - executeOnFirstTick: bool = False, - trackOutput: bool = True, -): + 将《我的世界》的游戏刻计转为表示时间的字符串 """ - 使用指定项目返回指定的指令方块放置指令项 - :param command: `str` - 指令 - :param particularValue: - 方块特殊值,即朝向 - :0 下 无条件 - :1 上 无条件 - :2 z轴负方向 无条件 - :3 z轴正方向 无条件 - :4 x轴负方向 无条件 - :5 x轴正方向 无条件 - :6 下 无条件 - :7 下 无条件 + return str(int(int(mc_tick / 20) / 60)) + ":" + str(int(int(mc_tick / 20) % 60)) - :8 下 有条件 - :9 上 有条件 - :10 z轴负方向 有条件 - :11 z轴正方向 有条件 - :12 x轴负方向 有条件 - :13 x轴正方向 有条件 - :14 下 有条件 - :14 下 有条件 - 注意!此处特殊值中的条件会被下面condition参数覆写 - :param impluse: `int 0|1|2` - 方块类型 - 0脉冲 1循环 2连锁 - :param condition: `bool` - 是否有条件 - :param needRedstone: `bool` - 是否需要红石 - :param tickDelay: `int` - 执行延时 - :param customName: `str` - 悬浮字 - lastOutput: `str` - 上次输出字符串,注意此处需要留空 - :param executeOnFirstTick: `bool` - 首刻执行(循环指令方块是否激活后立即执行,若为False,则从激活时起延迟后第一次执行) - :param trackOutput: `bool` - 是否输出 - :return:str +def empty_midi_channels(channel_count: int = 17) -> dict: """ - block = b"\x24" + particularValue.to_bytes(2, byteorder="big", signed=False) - - for i in [ - impluse.to_bytes(4, byteorder="big", signed=False), - bytes(command, encoding="utf-8") + b"\x00", - bytes(customName, encoding="utf-8") + b"\x00", - bytes("", encoding="utf-8") + b"\x00", - tickDelay.to_bytes(4, byteorder="big", signed=True), - executeOnFirstTick.to_bytes(1, byteorder="big"), - trackOutput.to_bytes(1, byteorder="big"), - condition.to_bytes(1, byteorder="big"), - needRedstone.to_bytes(1, byteorder="big"), - ]: - block += i - return block - - -def bottem_side_length_of_smallest_square_bottom_box(total: int, maxHeight: int): - """给定总方块数量和最大高度,返回所构成的图形外切正方形的边长 - :param total: 总方块数量 - :param maxHeight: 最大高度 - :return: 外切正方形的边长 int""" - return math.ceil(math.sqrt(math.ceil(total / maxHeight))) - - -def commands_to_BDX_bytes( - commands: list, - max_height: int = 64, -): + 空MIDI通道字典 """ - :param commands: 指令列表(指令, 延迟) - :param max_height: 生成结构最大高度 - :return 成功与否,成功返回(True,未经过压缩的源,结构占用大小),失败返回(False,str失败原因) - """ - - _sideLength = bottem_side_length_of_smallest_square_bottom_box( - len(commands), max_height - ) - _bytes = b"" - - y_forward = True - z_forward = True - - now_y = 0 - now_z = 0 - now_x = 0 - - for cmd, delay in commands: - impluse = 2 - condition = False - needRedstone = False - tickDelay = delay - customName = "" - executeOnFirstTick = False - trackOutput = True - _bytes += form_command_block_in_BDX_bytes( - cmd, - (1 if y_forward else 0) - if ( - ((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, - impluse=impluse, - condition=condition, - needRedstone=needRedstone, - tickDelay=tickDelay, - customName=customName, - executeOnFirstTick=executeOnFirstTick, - trackOutput=trackOutput, - ) - - now_y += 1 if y_forward else -1 - - if ((now_y >= max_height) and y_forward) or ((now_y < 0) and (not y_forward)): - now_y -= 1 if y_forward else -1 - - y_forward = not y_forward - - now_z += 1 if z_forward else -1 - - if ((now_z >= _sideLength) and z_forward) or ( - (now_z < 0) and (not z_forward) - ): - now_z -= 1 if z_forward else -1 - z_forward = not z_forward - _bytes += bdx_key[x][1] - now_x += 1 - else: - _bytes += bdx_key[z][int(z_forward)] - - else: - _bytes += bdx_key[y][int(y_forward)] - - return ( - _bytes, - [ - now_x + 1, - max_height if now_x or now_z else now_y, - _sideLength if now_x else now_z, - ], - [now_x, now_y, now_z], - ) - - -def form_note_block_in_NBT_struct( - note: int, coordinate: tuple, instrument: str = "note.harp", powered: bool = False -): - """生成音符盒方块 - :param note: `int`(0~24) - 音符的音高 - :param coordinate: `tuple[int,int,int]` - 此方块所在之相对坐标 - :param instrument: `str` - 音符盒的乐器 - :param powered: `bool` - 是否已被激活 - :return Block - """ - - from TrimMCStruct import Block, TAG_Byte - return Block( - "minecraft", - "noteblock", - { - "instrument": instrument.replace("note.", ""), - "note": note, - "powered": powered, - }, - { - "block_entity_data": { - "note": TAG_Byte(note), - "id": "noteblock", - "x": coordinate[0], - "y": coordinate[1], - "z": coordinate[2], - } - }, - ) - - -def form_repeater_in_NBT_struct( - delay: int, facing: int -): - """生成中继器方块 - :param facing: - :param delay: 1~4 - :return Block()""" - - from TrimMCStruct import Block - - return Block( - "minecraft", - "unpowered_repeater", - { - "repeater_delay": delay, - "direction": facing, - }, - ) - - -def form_command_block_in_NBT_struct( - command: str, - coordinate: tuple, - particularValue: int, - impluse: int = 0, - condition: bool = False, - alwaysRun: bool = True, - tickDelay: int = 0, - customName: str = "", - executeOnFirstTick: bool = False, - trackOutput: bool = True, -): - """ - 使用指定项目返回指定的指令方块结构 - :param command: `str` - 指令 - :param coordinate: `tuple[int,int,int]` - 此方块所在之相对坐标 - :param particularValue: - 方块特殊值,即朝向 - :0 下 无条件 - :1 上 无条件 - :2 z轴负方向 无条件 - :3 z轴正方向 无条件 - :4 x轴负方向 无条件 - :5 x轴正方向 无条件 - :6 下 无条件 - :7 下 无条件 - - :8 下 有条件 - :9 上 有条件 - :10 z轴负方向 有条件 - :11 z轴正方向 有条件 - :12 x轴负方向 有条件 - :13 x轴正方向 有条件 - :14 下 有条件 - :14 下 有条件 - 注意!此处特殊值中的条件会被下面condition参数覆写 - :param impluse: `int 0|1|2` - 方块类型 - 0脉冲 1循环 2连锁 - :param condition: `bool` - 是否有条件 - :param alwaysRun: `bool` - 是否始终执行 - :param tickDelay: `int` - 执行延时 - :param customName: `str` - 悬浮字 - :param executeOnFirstTick: `bool` - 首刻执行(循环指令方块是否激活后立即执行,若为False,则从激活时起延迟后第一次执行) - :param trackOutput: `bool` - 是否输出 - - :return:str - """ - - from TrimMCStruct import Block, TAG_Long - - return Block( - "minecraft", - "command_block" - if impluse == 0 - else ("repeating_command_block" if impluse == 1 else "chain_command_block"), - states={"conditional_bit": condition, "facing_direction": particularValue}, - extra_data={ - "block_entity_data": { - "Command": command, - "CustomName": customName, - "ExecuteOnFirstTick": executeOnFirstTick, - "LPCommandMode": 0, - "LPCondionalMode": False, - "LPRedstoneMode": False, - "LastExecution": TAG_Long(0), - "LastOutput": "", - "LastOutputParams": [], - "SuccessCount": 0, - "TickDelay": tickDelay, - "TrackOutput": trackOutput, - "Version": 25, - "auto": alwaysRun, - "conditionMet": False, # 是否已经满足条件 - "conditionalMode": condition, - "id": "CommandBlock", - "isMovable": True, - "powered": False, # 是否已激活 - "x": coordinate[0], - "y": coordinate[1], - "z": coordinate[2], - } - }, - compability_version=17959425, - ) - - -def commands_to_structure( - commands: list, - max_height: int = 64, -): - """ - :param commands: 指令列表(指令, 延迟) - :param max_height: 生成结构最大高度 - :return 成功与否,成功返回(结构类,结构占用大小),失败返回(False,str失败原因) - """ - - from TrimMCStruct import Structure - - _sideLength = bottem_side_length_of_smallest_square_bottom_box( - len(commands), max_height - ) - - struct = Structure( - (_sideLength, max_height, _sideLength), # 声明结构大小 - ) - - y_forward = True - z_forward = True - - now_y = 0 - now_z = 0 - now_x = 0 - - for cmd, delay in commands: - coordinate = (now_x, now_y, now_z) - struct.set_block( - coordinate, - form_command_block_in_NBT_struct( - command=cmd, - coordinate=coordinate, - particularValue=(1 if y_forward else 0) - if ( - ((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 - ), - impluse=2, - condition=False, - alwaysRun=True, - tickDelay=delay, - customName="", - executeOnFirstTick=False, - trackOutput=True, - ), - ) - - now_y += 1 if y_forward else -1 - - if ((now_y >= max_height) and y_forward) or ((now_y < 0) and (not y_forward)): - now_y -= 1 if y_forward else -1 - - y_forward = not y_forward - - now_z += 1 if z_forward else -1 - - if ((now_z >= _sideLength) and z_forward) or ( - (now_z < 0) and (not z_forward) - ): - now_z -= 1 if z_forward else -1 - z_forward = not z_forward - now_x += 1 - - return ( - struct, - ( - now_x + 1, - max_height if now_x or now_z else now_y, - _sideLength if now_x else now_z, - ), - (now_x, now_y, now_z), - ) + return dict((i, {}) for i in range(channel_count)) diff --git a/README.md b/README.md index 436320a..95d6e51 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,3 @@ -
@@ -18,12 +17,8 @@
-
-
-
-
[![][Bilibili: 金羿ELS]](https://space.bilibili.com/397369002/)
-[![][Bilibili: 诸葛亮与八卦阵]](https://space.bilibili.com/604072474)
+[![][Bilibili: 诸葛亮与八卦阵]](https://space.bilibili.com/604072474)
[![CodeStyle: black]](https://github.com/psf/black)
[![][python]](https://www.python.org/)
[![][license]](LICENSE)
@@ -34,96 +29,96 @@
[](https://github.com/TriM-Organization/Musicreater/stargazers)
[](https://github.com/TriM-Organization/Musicreater/forks)
+简体中文 🇨🇳 | [English🇬🇧](README_EN.md)
-简体中文🇨🇳 | [English🇬🇧](README_EN.md)
+## 介绍 🚀
-
-## 介绍🚀
-
-音·创 是一个免费开源的针对 **《我的世界》** 的MIDI音乐转换库
+音·创 是一个免费开源的针对 **《我的世界》** 的 MIDI 音乐转换库
欢迎加群:[861684859](https://jq.qq.com/?_wv=1027&k=hpeRxrYr)
-## 下载安装
+## 安装 🔳
-- 使用pypi
- ```bash
- pip install Musicreater
- ```
+- 使用 pypi
+
+ ```bash
+ pip install Musicreater
+ ```
- 如果出现错误,可以尝试:
- ```bash
- pip install -i https://pypi.python.org/simple Musicreater
- ```
-- (对于开发者来说)升级:
- ```bash
- pip install -i https://pypi.python.org/simple Musicreater --upgrade
- ```
+ ```bash
+ pip install -i https://pypi.python.org/simple Musicreater
+ ```
+
+- 升级:
+
+ ```bash
+ pip install -i https://pypi.python.org/simple Musicreater --upgrade
+ ```
- 克隆仓库并安装
- ```bash
- git clone https://gitee.com/TriM-Organization/Musicreater.git
- cd Musicreater
- python setup.py install
- ```
+ ```bash
+ git clone https://gitee.com/TriM-Organization/Musicreater.git
+ cd Musicreater
+ python setup.py install
+ ```
-以上命令种 `python`、`pip` 请依照各个环境不同灵活更换,可能为`python3`或`pip3`之类。
+以上命令中 `python`、`pip` 请依照各个环境不同灵活更换,可能为`python3`或`pip3`之类。
-## 文档📄
+## 文档 📄
[生成文件的使用](./docs/%E7%94%9F%E6%88%90%E6%96%87%E4%BB%B6%E7%9A%84%E4%BD%BF%E7%94%A8%E8%AF%B4%E6%98%8E.md)
-[仓库API文档](./docs/%E5%BA%93%E7%9A%84%E7%94%9F%E6%88%90%E4%B8%8E%E5%8A%9F%E8%83%BD%E6%96%87%E6%A1%A3.md)
+[仓库 API 文档](./docs/%E5%BA%93%E7%9A%84%E7%94%9F%E6%88%90%E4%B8%8E%E5%8A%9F%E8%83%BD%E6%96%87%E6%A1%A3.md)
-## 作者✒
+## 作者 ✒
-金羿 Eilles:我的世界基岩版指令师,个人开发者,B站不知名UP主,江西在校高中生。
+金羿 Eilles:我的世界基岩版指令师,个人开发者,B 站不知名 UP 主,江西在校高中生。
诸葛亮与八卦阵 bgArray:我的世界基岩版玩家,喜欢编程和音乐,深圳初二学生。
-## 致谢🙏
+## 致谢 🙏
+
本致谢列表排名无顺序。
-- 感谢 **昀梦**\
-
-
[![][Bilibili: Eilles]](https://space.bilibili.com/397369002/)
-[![][Bilibili: bgArray]](https://space.bilibili.com/604072474)
+[![][Bilibili: bgArray]](https://space.bilibili.com/604072474)
[![CodeStyle: black]](https://github.com/psf/black)
-![][python]
+[![][python]](https://www.python.org/)
[![][license]](LICENSE)
[![][release]](../../releases)
-[简体中文🇨🇳](README.md) | English🇬🇧
+[](https://gitee.com/TriM-Organization/Musicreater/stargazers)
+[](https://gitee.com/TriM-Organization/Musicreater/members)
+[](https://github.com/TriM-Organization/Musicreater/stargazers)
+[](https://github.com/TriM-Organization/Musicreater/forks)
-**Notice that the language translation of *Musicreater* may be a little SLOW.**
+[简体中文 🇨🇳](README.md) | English🇬🇧
+
+**Notice that the language translation of _Musicreater_ may be a little SLOW.**
## Introduction🚀
-Musicreater is a free open-source library used for converting midi file into formats that could be read in *Minecraft*.
+Musicreater is a free open-source library used for converting digital music files into formats that could be read in _Minecraft_.
Welcome to join our QQ group: [861684859](https://jq.qq.com/?_wv=1027&k=hpeRxrYr)
+## Installation 🔳
+
+- Via pypi
+
+ ```bash
+ pip install Musicreater
+ ```
+
+- If not work, also try:
+ ```bash
+ pip install -i https://pypi.python.org/simple Musicreater
+ ```
+
+- Update:
+
+ ```bash
+ pip install -i https://pypi.python.org/simple Musicreater --upgrade
+ ```
+
+- Clone repo and Install:
+ ```bash
+ git clone https://github.com/TriM-Organization/Musicreater.git
+ cd Musicreater
+ python setup.py install
+ ```
+
+Commands such as `python`、`pip` could be changed to some like `python3` or `pip3` according to the difference of platforms.
+
+
## Documentation📄
(Not in English yet)
[生成文件的使用](./docs/%E7%94%9F%E6%88%90%E6%96%87%E4%BB%B6%E7%9A%84%E4%BD%BF%E7%94%A8%E8%AF%B4%E6%98%8E.md)
-[仓库API文档](./docs/%E5%BA%93%E7%9A%84%E7%94%9F%E6%88%90%E4%B8%8E%E5%8A%9F%E8%83%BD%E6%96%87%E6%A1%A3.md)
+[仓库 API 文档](./docs/%E5%BA%93%E7%9A%84%E7%94%9F%E6%88%90%E4%B8%8E%E5%8A%9F%E8%83%BD%E6%96%87%E6%A1%A3.md)
### Authors✒
-Eilles (金羿):A senior high school student, individual developer, unfamous Bilibili UPer, which knows a little about commands in *Minecraft: Bedrock Edition*
-
-bgArray "诸葛亮与八卦阵": A junior high school student, player of *Minecraft: Bedrock Edition*, which is a fan of music and programming.
+Eilles (金羿):A senior high school student, individual developer, unfamous Bilibili UPer, which knows a little about commands in _Minecraft: Bedrock Edition_
+bgArray "诸葛亮与八卦阵": A junior high school student, player of _Minecraft: Bedrock Edition_, which is a fan of music and programming.
## Thanks🙏
+
This list is not in any order.
-- Thank *昀梦*\音·创 Musicreater
+
+ 音·创 Musicreater
+
+
+
a free open-source library of converting midi files into _Minecraft_ formats.
+A free open-source library of converting digital music files into Minecraft formats.
+
+