diff --git a/magicDemo.py b/magicDemo.py index 85ca4b0..dafc015 100644 --- a/magicDemo.py +++ b/magicDemo.py @@ -126,7 +126,6 @@ MainConsole.rule( ) nowYang = datetime.datetime.now() -# nowYin = zhdate.ZhDate.from_datetime(nowYang) if nowYang.month == 8 and nowYang.day == 6: # 诸葛八卦生日 @@ -142,18 +141,6 @@ elif nowYang.month == 4 and nowYang.day == 3: style="#0089F2 on #F0F2F4", justify="center", ) -# elif nowYin.lunar_month == 12 and nowYin.lunar_day == 30: -# MainConsole.print( -# "[#FF3432 on #121110]除夕到了,你是否与家人共处,融融其乐?", -# style="#FF3432 on #121110", -# justify="center", -# ) -# elif nowYin.leap_month == 1 and nowYin.lunar_day in range(1, 9): -# MainConsole.print( -# "[#FFF642 on #FF3432]春节快乐!\n在你使用音·创的时候,是不是也要去感受一下喜庆的氛围呢?", -# style="#FFF642 on #FF3432", -# justify="center", -# ) else: # 显示箴言部分 MainConsole.print( @@ -334,7 +321,7 @@ for singleMidi in midis: else ( conversion.toBDXfile(2, *prompts) if playerFormat == 1 - else conversion.toBDXfile_withDelay(1, *prompts) + else conversion.toBDXfile_withDelay(2, *prompts) ) ) @@ -345,7 +332,11 @@ for singleMidi in midis: else: prt(f"{_('Failed')}") -if ipt(_("PressEnterExit")).lower() == "record": + +exitSth = ipt(_("PressEnterExit")).lower() +if exitSth == "record": import json with open("./demo_config.json",'w',encoding="utf-8") as f: json.dump(prompts,f) +elif exitSth == "delrec": + os.remove("./demo_config.json") diff --git a/msctPkgver/__init__.py b/msctPkgver/__init__.py index b5c2989..c42c324 100644 --- a/msctPkgver/__init__.py +++ b/msctPkgver/__init__.py @@ -7,7 +7,7 @@ # 若需转载或借鉴 许可声明请查看仓库目录下的 Lisence.md -__version__ = '0.2.1.4' +__version__ = '0.2.2' __all__ = [] __author__ = (('金羿', 'Eilles Wan'), ('诸葛亮与八卦阵', 'bgArray'), ('鸣凤鸽子', 'MingFengPigeon')) diff --git a/msctPkgver/exceptions.py b/msctPkgver/exceptions.py index dfddf46..0f5d451 100644 --- a/msctPkgver/exceptions.py +++ b/msctPkgver/exceptions.py @@ -13,7 +13,8 @@ Musicreater pkgver (Package Version 音·创 库版) A free open source library used for convert midi file into formats that is suitable for **Minecraft: Bedrock Edition**. -Copyright 2023 all the developers of Musicreater +版权所有 © 2023 音·创 开发者 +Copyright © 2023 all the developers of Musicreater 开源相关声明请见 ../Lisence.md Terms & Conditions: ../Lisence.md diff --git a/msctPkgver/main.py b/msctPkgver/main.py index 986d125..0f38b3f 100644 --- a/msctPkgver/main.py +++ b/msctPkgver/main.py @@ -13,7 +13,8 @@ Musicreater pkgver (Package Version 音·创 库版) A free open source library used for convert midi file into formats that is suitable for **Minecraft: Bedrock Edition**. -Copyright 2023 all the developers of Musicreater +版权所有 © 2023 音·创 开发者 +Copyright © 2023 all the developers of Musicreater 开源相关声明请见 ../Lisence.md Terms & Conditions: ../Lisence.md @@ -122,6 +123,7 @@ class midiConvert: ] self.methods_byDelay = [ self._toCmdList_withDelay_m1, + self._toCmdList_withDelay_m2, ] if self.debugMode: from .magicBeing import prt, ipt @@ -425,7 +427,7 @@ class midiConvert: ) result.append( self.exeHead.format("@a[scores={" + scoreboardname + "=1..}]") - + "scoreboard players set MaxScore {} 100".format(scoreboardname) + + "scoreboard players set n100 {} 100".format(scoreboardname) ) result.append( self.exeHead.format("@a[scores={" + scoreboardname + "=1..}]") @@ -548,7 +550,7 @@ class midiConvert: return result def _toCmdList_m1( - self, scoreboardname: str = "mscplay", volume: float = 1.0, speed: float = 1.0 + self, scoreboardname: str = "mscplay", MaxVolume: float = 1.0, speed: float = 1.0 ) -> list: """ 使用Dislink Sforza的转换思路,将midi转换为我的世界命令列表 @@ -558,10 +560,7 @@ class midiConvert: :return: tuple(命令列表, 命令个数, 计分板最大值) """ tracks = [] - if volume > 1: - volume = 1 - if volume <= 0: - volume = 0.001 + MaxVolume = 1 if MaxVolume > 1 else (0.001 if MaxVolume <= 0 else MaxVolume) commands = 0 maxscore = 0 @@ -603,7 +602,7 @@ class midiConvert: + "=" + str(nowscore) + "}" - + f"] ~ ~ ~ playsound {soundID} @s ^ ^ ^{1 / volume - 1} {msg.velocity/128} {2 ** ((msg.note - 60 - _X) / 12)}" + + f"] ~ ~ ~ playsound {soundID} @s ^ ^ ^{1 / MaxVolume - 1} {msg.velocity/128} {2 ** ((msg.note - 60 - _X) / 12)}" ) commands += 1 if len(singleTrack) != 0: @@ -954,33 +953,21 @@ class midiConvert: def _toCmdList_withDelay_m1( self, - volume: float = 1.0, + MaxVolume: float = 1.0, speed: float = 1.0, player: str = "@a", - # isMixedWithPrograssBar=False, ) -> list: """ 使用Dislink Sforza的转换思路,将midi转换为我的世界命令列表,并输出每个音符之后的延迟 :param volume: 音量,注意:这里的音量范围为(0,1],如果超出将被处理为正确值,其原理为在距离玩家 (1 / volume -1) 的地方播放音频 :param speed: 速度,注意:这里的速度指的是播放倍率,其原理为在播放音频的时候,每个音符的播放时间除以 speed :param player: 玩家选择器,默认为`@a` - :param isMixedWithPrograssBar: 进度条,(当此参数为True时使用默认进度条,当此参数为其他值为真的表达式时识别为进度条自定义参数,若为其他值为假的表达式则不生成进度条) :return: 全部指令列表[ ( str指令, int距离上一个指令的延迟 ),...] """ tracks = {} - if volume > 1: - volume = 1 - if volume <= 0: - volume = 0.001 - - # 此处是对于仅有 True 的参数和自定义参数的判断 - # if isMixedWithPrograssBar == True: - # isMixedWithPrograssBar = ( - # r"▶ %%N [ %%s/%^s %%% __________ %%t|%^t ]", - # ("§e=§r", "§7=§r"), - # ) - + MaxVolume = 1 if MaxVolume > 1 else (0.001 if MaxVolume <= 0 else MaxVolume) + for i, track in enumerate(self.midi.tracks): instrumentID = 0 @@ -1002,18 +989,16 @@ class midiConvert: soundID, _X = self.__Inst2soundIDwithX(instrumentID) try: tracks[nowtick].append( - f"execute {player} ~ ~ ~ playsound {soundID} @s ~ ~{1 / volume - 1} ~ {msg.velocity * (0.7 if msg.channel == 0 else 0.9)} {2 ** ((msg.note - 60 - _X) / 12)}" + self.exeHead.format(player)+f"playsound {soundID} @s ^ ^ ^{1 / MaxVolume - 1} {msg.velocity/128} {2 ** ((msg.note - 60 - _X) / 12)}" ) except BaseException: - tracks[nowtick] = [ - f"execute {player} ~ ~ ~ playsound {soundID} @s ~ ~{1 / volume - 1} ~ {msg.velocity * (0.7 if msg.channel == 0 else 0.9)} {2 ** ((msg.note - 60 - _X) / 12)}", + tracks[nowtick]= [ + self.exeHead.format(player)+f"playsound {soundID} @s ^ ^ ^{1 / MaxVolume - 1} {msg.velocity/128} {2 ** ((msg.note - 60 - _X) / 12)}" ] results = [] allticks = list(tracks.keys()) - # if isMixedWithPrograssBar: - # results.append("scoreboard objectives add {}") for i in range(len(allticks)): if i != 0: @@ -1030,6 +1015,170 @@ class midiConvert: return results, max(allticks) + def _toCmdList_withDelay_m2( + self, + MaxVolume: float = 1.0, + speed: float = 1.0, + player: str = "@a", + ) -> list: + """ + 使用金羿的转换思路,将midi转换为我的世界命令列表,并输出每个音符之后的延迟 + :param volume: 音量,注意:这里的音量范围为(0,1],如果超出将被处理为正确值,其原理为在距离玩家 (1 / volume -1) 的地方播放音频 + :param speed: 速度,注意:这里的速度指的是播放倍率,其原理为在播放音频的时候,每个音符的播放时间除以 speed + :param player: 玩家选择器,默认为`@a` + :return: 全部指令列表[ ( str指令, int距离上一个指令的延迟 ),...] + """ + tracks = {} + + 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: + + if msg.time != 0: + try: + microseconds += msg.time * tempo / self.midi.ticks_per_beat + # print(microseconds) + except NameError: + if self.debugMode: + raise NotDefineTempoError("计算当前分数时出错 未定义参量 Tempo") + else: + microseconds += ( + msg.time + * mido.midifiles.midifiles.DEFAULT_TEMPO + / self.midi.ticks_per_beat + ) + + if msg.is_meta: + if msg.type == "set_tempo": + tempo = msg.tempo + if self.debugMode: + self.prt(f"TEMPO更改:{tempo}(毫秒每拍)") + else: + + if self.debugMode: + try: + if msg.channel > 15: + raise ChannelOverFlowError(f"当前消息 {msg} 的通道超限(≤15)") + except: + 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.__bitInst2IDwithX(InstID) + if SpecialBits + else self.__Inst2soundIDwithX(InstID) + ) + except UnboundLocalError as E: + if self.debugMode: + raise NotDefineProgramError(f"未定义乐器便提前演奏。\n{E}") + else: + soundID, _X = ( + self.__bitInst2IDwithX(-1) + if SpecialBits + else self.__Inst2soundIDwithX(-1) + ) + score_now = round(msg[-1] / float(speed) / 50) + + try: + tracks[score_now].append( + self.exeHead.format(player)+f"playsound {soundID} @s ^ ^ ^{1 / MaxVolume - 1} {msg.velocity/128} {2 ** ((msg.note - 60 - _X) / 12)}" + ) + except BaseException: + tracks[score_now]= [ + self.exeHead.format(player)+f"playsound {soundID} @s ^ ^ ^{1 / MaxVolume - 1} {msg.velocity/128} {2 ** ((msg.note - 60 - _X) / 12)}" + ] + + cmdAmount += 1 + + + + + allticks = list(tracks.keys()) + + for i in range(len(allticks)): + if i != 0: + for j in range(len(tracks[allticks[i]])): + if j != 0: + results.append((tracks[allticks[i]][j], 0)) + else: + results.append( + (tracks[allticks[i]][j], allticks[i] - allticks[i - 1]) + ) + else: + for j in range(len(tracks[allticks[i]])): + results.append((tracks[allticks[i]][j], allticks[i])) + + return results, max(allticks) + + def tomcpack( self, method: int = 1, diff --git a/msctPkgver/utils_future.py b/msctPkgver/utils_future.py new file mode 100644 index 0000000..358ada5 --- /dev/null +++ b/msctPkgver/utils_future.py @@ -0,0 +1,348 @@ +import math +import os +import brotli + +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 move(axis: str, value: int): + if value == 0: + return b'' + if abs(value) == 1: + return 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 key[axis][pointer] + value.to_bytes(2 ** (pointer - 2), 'big', signed=True) + + +def makeZip(sourceDir, outFilename, compression=8, exceptFile=None): + """使用compression指定的算法打包目录为zip文件\n + 默认算法为DEFLATED(8),可用算法如下:\n + STORED = 0\n + DEFLATED = 8\n + BZIP2 = 12\n + LZMA = 14\n + """ + 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) + arcname = pathfile[pre_len:].strip(os.path.sep) # 相对路径 + zipf.write(pathfile, arcname) + zipf.close() + + +def formCMDblk( + 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 __fillSquareSideLength(total: int, maxHeight: int): + """给定总方块数量和最大高度,返回所构成的图形外切正方形的边长 + :param total: 总方块数量 + :param maxHeight: 最大高度 + :return: 外切正方形的边长 int""" + return math.ceil(math.sqrt(math.ceil(total / maxHeight))) + + +axisParticularValue = { + x: { + True: 5, + False: 4, + }, + y: { + True: 1, + False: 0, + }, + z: { + True: 3, + False: 2, + }, +} + + +def toLineBDXbytes( + commands: list, + axis: str, + forward: bool, +): + _bytes = b'' + for cmd, condition in commands: + _bytes += formCMDblk( + cmd, + axisParticularValue[axis][forward], + impluse=2, + condition=condition, + needRedstone=False, + tickDelay=0, + customName="", + executeOnFirstTick=False, + trackOutput=True, + ) + move(axis, 1 if forward else -1) + return _bytes + + +def toBDXbytes( + commands: list, + maxheight: int = 64, +): + """ + :param commands: 指令列表(指令, 条件) + :param maxheight: 生成结构最大高度 + :return 成功与否,成功返回(True,未经过压缩的源,结构占用大小),失败返回(False,str失败原因) + """ + + _sideLength = __fillSquareSideLength(len(commands), maxheight) + _bytes = b'' + + yforward = True + zforward = True + + nowy = 0 + nowz = 0 + nowx = 0 + + for cmd, condition in commands: + _bytes += formCMDblk( + cmd, + (1 if yforward else 0) + if ( + ((nowy != 0) and (not yforward)) + or ((yforward) and (nowy != (maxheight - 1))) + ) + else (3 if zforward else 2) + if ( + ((nowz != 0) and (not zforward)) + or ((zforward) and (nowz != _sideLength)) + ) + else 5, + impluse=2, + condition=condition, + needRedstone=False, + tickDelay=0, + customName="", + executeOnFirstTick=False, + trackOutput=True, + ) + + nowy += 1 if yforward else -1 + + if ((nowy >= maxheight) and (yforward)) or ((nowy < 0) and (not yforward)): + nowy -= 1 if yforward else -1 + + yforward = not yforward + + nowz += 1 if zforward else -1 + + if ((nowz > _sideLength) and (zforward)) or ((nowz < 0) and (not zforward)): + nowz -= 1 if zforward else -1 + zforward = not zforward + _bytes += key[x][1] + nowx += 1 + else: + + _bytes += key[z][int(zforward)] + + else: + + _bytes += key[y][int(yforward)] + + return ( + _bytes, + [nowx + 1, maxheight if nowx or nowz else nowy, _sideLength if nowx else nowz], + [nowx, nowy, nowz], + ) + + + + +def toBDXfile( + funcList: list, + author: str = "Eilles", + maxheight: int = 64, + outfile: str = "./test.bdx", +): + """ + :funcList list: 指令集列表: 指令系统[ 指令集[ 单个指令( str指令, bool条件性 ), ], ] + :param author: 作者名称 + :param maxheight: 生成结构最大高度 + :outfile: str 输出文件 + :return 成功与否,指令总长度,指令总延迟,指令结构总大小,画笔最终坐标 + """ + + with open(os.path.abspath(outfile), "w+", encoding="utf-8") as f: + f.write("BD@") + + _bytes = ( + b"BDX\x00" + author.encode("utf-8") + b" & Musicreater\x00\x01command_block\x00" + ) + totalSize = {x: 0, y: 0, z: 0} + totalLen = 0 + for func in funcList: + totalLen += len(func) + cmdBytes, size, finalPos = toBDXbytes(func, maxheight) + _bytes += cmdBytes + _bytes += move(x, 2) + _bytes += move(y, -finalPos[1]) + _bytes += move(z, -finalPos[2]) + totalSize[x] += size[0] + 2 + totalSize[y] = max(totalSize[y], size[1]) + totalSize[z] = max(totalSize[z], size[2]) + + with open( + os.path.abspath(outfile), + "ab+", + ) as f: + f.write(brotli.compress(_bytes + b"XE")) + + return (True, totalLen, 0, list(totalSize.values()), finalPos) + + + +def toLineBDXfile( + funcList: list, + axis_: str, + forward_: bool, + author: str = "Eilles", + outfile: str = "./test.bdx", +): + """ + :funcList list: 指令集列表: 指令系统[ 指令集[ 单个指令( str指令, bool条件性 ), ], ] + :param author: 作者名称 + :param maxheight: 生成结构最大高度 + :outfile: str 输出文件 + :return 成功与否,指令总长度,指令总延迟,指令结构总大小,画笔最终坐标 + """ + + with open(os.path.abspath(outfile), "w+", encoding="utf-8") as f: + f.write("BD@") + + _bytes = ( + b"BDX\x00" + author.encode("utf-8") + b" & Musicreater\x00\x01command_block\x00" + ) + totalSize = {x: 0, y: 0, z: 0} + totalLen = 0 + for func in funcList: + totalLen += len(func) + _bytes += toLineBDXbytes(func, axis_, forward_) + _bytes += move(z if axis_ == x else x, 2) + + totalSize[z if axis_ == x else x] += 2 + totalSize[axis_] = max(totalSize[axis_], len(func)) + + with open( + os.path.abspath(outfile), + "ab+", + ) as f: + f.write(brotli.compress(_bytes + b"XE")) + + return (True, totalLen, 0, list(totalSize.values())) + +def formatipt(notice: str, fun, errnote: str = "", *extraArg): + '''循环输入,以某种格式 + notice: 输入时的提示 + fun: 格式函数 + errnote: 输入不符格式时的提示 + *extraArg: 对于函数的其他参数''' + while True: + result = input(notice) + try: + funresult = fun(result, *extraArg) + break + except: + print(errnote) + continue + return result, funresult