diff --git a/README_EN.md b/README_EN.md index c6c5036..054fb62 100644 --- a/README_EN.md +++ b/README_EN.md @@ -44,9 +44,9 @@ A simple Python package. > > During installation, be sure to check "add Python 3.X to path", otherwise it needs to be set manually > -> At the same time, after the installation, remember to enter in CMD: "Python" to try +> At the same time, after the installation, remember to enter in CMD: "python" to try > -> whether the installation is successful, +> whether the installation is successful. > > Python installation tutorial can be found on the Internet easily. > diff --git a/demo_convert.py b/demo_convert.py index b1ec441..6719c85 100644 --- a/demo_convert.py +++ b/demo_convert.py @@ -84,7 +84,6 @@ while True: print('输入错误,请重新输入') - if os.path.isdir(midipath): for i in os.listdir(midipath): if i.endswith('.mid'): @@ -93,8 +92,12 @@ if os.path.isdir(midipath): if outFormat == 0: convertion.tomcpack( 1, - isAutoReset if isAutoReset != '' else bool(int(input('是否自动重置计分板(1|0):'))), - isProgress if isProgress != '' else bool(int(input('是否开启进度条(1|0):'))), + isAutoReset + if isAutoReset != '' + else bool(int(input('是否自动重置计分板(1|0):'))), + isProgress + if isProgress != '' + else bool(int(input('是否开启进度条(1|0):'))), sbname if sbname != '' else input('请输入计分板名称:'), volume if volume != '' else float(input('请输入音量(0-1):')), speed if speed != '' else float(input('请输入速度倍率:')), @@ -103,12 +106,16 @@ if os.path.isdir(midipath): convertion.toBDXfile( 1, author if author != '' else input('请输入作者:'), - isProgress if isProgress != '' else bool(int(input('是否开启进度条(1|0):'))), + isProgress + if isProgress != '' + else bool(int(input('是否开启进度条(1|0):'))), maxHeight if maxHeight != '' else int(input('请输入指令结构最大生成高度:')), sbname if sbname != '' else input('请输入计分板名称:'), volume if volume != '' else float(input('请输入音量(0-1):')), speed if speed != '' else float(input('请输入速度倍率:')), - isAutoReset if isAutoReset != '' else bool(int(input('是否自动重置计分板(1|0):'))), + isAutoReset + if isAutoReset != '' + else bool(int(input('是否自动重置计分板(1|0):'))), ) else: convertion.convert(midipath, outpath) @@ -131,4 +138,4 @@ else: volume if volume != '' else float(input('请输入音量(0-1):')), speed if speed != '' else float(input('请输入速度倍率:')), isAutoReset if isAutoReset != '' else bool(int(input('是否自动重置计分板(1|0):'))), - ) \ No newline at end of file + ) diff --git a/example_convert_bdx_byDelay.py b/example_convert_bdx_byDelay.py new file mode 100644 index 0000000..a73fb46 --- /dev/null +++ b/example_convert_bdx_byDelay.py @@ -0,0 +1,17 @@ +# THIS PROGRAM IS ONLY A TEST EXAMPLE + + +from msctPkgver.main import * + +convertion = midiConvert() +convertion.convert(input('请输入midi文件路径:'), input('请输入输出路径:')) +for i in convertion.toBDXfile_withDelay( + 1, + input('请输入作者:'), + bool(int(input('是否开启进度条(1|0):'))), + int(input('请输入指令结构最大生成高度:')), + float(input('请输入音量(0-1]:')), + float(input('请输入速度倍率:')), + input('请输入玩家选择器(例@a[tag=ply]):'), +): + print(i) diff --git a/msctPkgver/main.py b/msctPkgver/main.py index dfa81db..726b63b 100644 --- a/msctPkgver/main.py +++ b/msctPkgver/main.py @@ -4,7 +4,7 @@ # 音·创 开发交流群 861684859 # Email EillesWan2006@163.com W-YI_DoctorYI@outlook.com EillesWan@outlook.com # 版权所有 金羿("Eilles Wan") & 诸葛亮与八卦阵("bgArray") & 鸣凤鸽子("MingFengPigeon") -# 若需转载或借鉴 请依照 Apache 2.0 许可证进行许可 +# 若需使用或借鉴 请依照 Apache 2.0 许可证进行许可 """ @@ -125,7 +125,9 @@ class midiConvert: ('§e=§r', '§7=§r'), ), ) -> list: + pgsstyle = progressbar[0] + '''用于被替换的进度条原始样式''' ''' | 标识符 | 指定的可变量 | @@ -184,6 +186,7 @@ class midiConvert: del idlist pgblength = pgsstyle.count('_') + '''进度条的“条”长度''' finalprgsbar = [] @@ -289,7 +292,7 @@ class midiConvert: self, scoreboardname: str = 'mscplay', volume: float = 1.0, speed: float = 1.0 ) -> list: """ - 使用被金羿修改后的Dislink Sforza的转换算法,将midi转换为我的世界命令列表 + 使用Dislink Sforza的转换思路,将midi转换为我的世界命令列表 :param scoreboardname: 我的世界的计分板名称 :param volume: 音量,注意:这里的音量范围为(0,1],如果超出将被处理为正确值,其原理为在距离玩家 (1 / volume -1) 的地方播放音频 :param speed: 速度,注意:这里的速度指的是播放倍率,其原理为在播放音频的时候,每个音符的播放时间除以 speed @@ -311,13 +314,13 @@ class midiConvert: singleTrack = [] for msg in track: + ticks += msg.time if msg.is_meta: if msg.type == 'set_tempo': tempo = msg.tempo if msg.type == 'program_change': instrumentID = msg.program else: - ticks += msg.time if msg.type == 'note_on' and msg.velocity != 0: nowscore = round( (ticks * tempo) @@ -338,50 +341,162 @@ class midiConvert: return tracks, commands, maxscore - def _toCmdList_withDelay_m1(self, volume: float = 1.0, speed: float = 1.0) -> list: + def _toCmdList_withDelay_m1( + self, + volume: float = 1.0, + speed: float = 1.0, + player: str = '@a', + isMixedWithPrograssBar=False, + ) -> list: """ - 使用Dislink Sforza的转换算法,将midi转换为我的世界命令列表,并输出每个音符之后的延迟 + 使用Dislink Sforza的转换思路,将midi转换为我的世界命令列表,并输出每个音符之后的延迟 :param volume: 音量,注意:这里的音量范围为(0,1],如果超出将被处理为正确值,其原理为在距离玩家 (1 / volume -1) 的地方播放音频 :param speed: 速度,注意:这里的速度指的是播放倍率,其原理为在播放音频的时候,每个音符的播放时间除以 speed - :return: tuple(命令列表[音轨[(命令,此命令的延迟),...],...], 命令个数,) + :param player: 玩家选择器,默认为`@a` + :param isMixedWithPrograssBar: 进度条,(当此参数为True时使用默认进度条,当此参数为其他值为真的表达式时识别为进度条自定义参数,若为其他值为假的表达式则不生成进度条) + :return: 全部指令列表[ ( str指令, int距离上一个指令的延迟 ),...] """ - tracks = [] + tracks = {} + if volume > 1: volume = 1 if volume <= 0: volume = 0.001 - commands = 0 - maxscore = 0 - for i, track in enumerate(self.midi.tracks): - ticks = 0 instrumentID = 0 - singleTrack = [] + ticks = 0 for msg in track: + timesnow += msg.time + ticks += msg.time if msg.is_meta: if msg.type == 'set_tempo': tempo = msg.tempo if msg.type == 'program_change': instrumentID = msg.program else: - ticks += msg.time if msg.type == 'note_on' and msg.velocity != 0: - nowscore = round( + nowtick = round( (ticks * tempo) / ((self.midi.ticks_per_beat * float(speed)) * 50000) ) - maxscore = max(maxscore, nowscore) - singleTrack.append( - f'playsound {self.__Inst2SoundID(instrumentID)} @s ~ ~{1/volume-1} ~ {msg.velocity*(0.7 if msg.channel == 0 else 0.9)} {2**((msg.note-66)/12)}' + try: + tracks[nowtick].append( + f'execute {player} ~ ~ ~ playsound {self.__Inst2SoundID(instrumentID)} @s ~ ~{1/volume-1} ~ {msg.velocity*(0.7 if msg.channel == 0 else 0.9)} {2**((msg.note-66)/12)}' + ) + except: + tracks[nowtick] = [ + f'execute {player} ~ ~ ~ playsound {self.__Inst2SoundID(instrumentID)} @s ~ ~{1/volume-1} ~ {msg.velocity*(0.7 if msg.channel == 0 else 0.9)} {2**((msg.note-66)/12)}', + ] + + allticks = list(tracks.keys()) + + if isMixedWithPrograssBar: + + pgsstyle = isMixedWithPrograssBar[0] + '''用于被替换的进度条原始样式''' + + ''' + | 标识符 | 指定的可变量 | + |---------|----------------| + | `%%N` | 乐曲名(即传入的文件名)| + | `%%s` | 当前计分板值 | + | `%^s` | 计分板最大值 | + | `%%t` | 当前播放时间 | + | `%^t` | 曲目总时长 | + | `%%%` | 当前进度比率 | + | `_` | 用以表示进度条占位| + ''' + + def __replace( + s: str, tobeReplaced: str, replaceWith: str, times: int, other: str + ): + if times == 0: + return s.replace(tobeReplaced, other) + if times == s.count(tobeReplaced): + return s.replace(tobeReplaced, replaceWith) + result = '' + t = 0 + for i in s: + if i == tobeReplaced: + if t < times: + result += replaceWith + t += 1 + else: + result += other + else: + result += i + + return result + + idlist = { + r'%%N': self.midFileName, + r'%%s': r'%%s', + r'%^s': str(allticks[-1]), + r'%%t': r'%%t', + r'%^t': self.__score2time(allticks[-1]), + r'%%%': r'%%%', + } + + ids = {} + + for i, j in idlist.items(): + if i != j: + if i in pgsstyle: + pgsstyle = pgsstyle.replace(i, j) + else: + if i in pgsstyle: + ids[i] = True + else: + ids[i] = False + + del idlist + + pgblength = pgsstyle.count('_') + '''进度条的“条”长度''' + + results = [] + + for i in range(len(allticks)): + if i != 0: + for j in range(len(tracks[allticks[i]])): + if i != 0: + results.append((tracks[allticks[i]][j], 0)) + else: + results.append( + (tracks[allticks[i]][j], allticks[i] - allticks[i - 1]) ) - commands += 1 + else: + for j in range(len(tracks[allticks[i]])): + results.append((tracks[allticks[i]][j], allticks[i])) - tracks.append(singleTrack) + if isMixedWithPrograssBar: - return tracks, commands, maxscore + nowstr = pgsstyle + if ids[r'%%s'] == True: + nowstr = nowstr.replace(r'%%s', str(i + 1)) + if ids[r'%%t'] == True: + nowstr = nowstr.replace(r'%%t', self.__score2time(i + 1)) + if ids[r'%%%'] == True: + nowstr = nowstr.replace( + r'%%%', str(int((i + 1) / allticks[-1] * 10000) / 100) + '%' + ) + + countof_s = int((i + 1) / allticks[-1] * pgblength) + + titlenow = __replace( + nowstr, + '_', + isMixedWithPrograssBar[1][0], + countof_s, + isMixedWithPrograssBar[1][1], + ) + + results.append(f'title {player} actionbar {titlenow}', 0) + + return results def __fillSquareSideLength(self, total: int, maxHeight: int): '''给定总方块数量和最大高度,返回所构成的图形外切正方形的边长 @@ -666,3 +781,116 @@ class midiConvert: f.write(brotli.compress(_bytes + b'XE')) return (True, _bytes, (nowx, maxheight, _sideLength)) + + def toBDXfile_withDelay( + self, + method: int = 1, + author: str = 'Eilles', + progressbar=False, + maxheight: int = 64, + volume: float = 1.0, + speed: float = 1.0, + player: str = '@a', + ): + """ + 使用method指定的转换算法,将midi转换为BDX结构文件 + :param method: 转换算法 + :param author: 作者名称 + :param progressbar: 进度条,(当此参数为True时使用默认进度条,为其他的值为真的参数时识别为进度条自定义参数,为其他值为假的时候不生成进度条) + :param maxheight: 生成结构最大高度 + :param volume: 音量,注意:这里的音量范围为(0,1],如果超出将被处理为正确值,其原理为在距离玩家 (1 / volume -1) 的地方播放音频 + :param speed: 速度,注意:这里的速度指的是播放倍率,其原理为在播放音频的时候,每个音符的播放时间除以 speed + :param player: 玩家选择器,默认为`@a` + :return 成功与否,成功返回(True,未经过压缩的源,结构占用大小),失败返回(False,str失败原因) + """ + + import brotli + + if method == 1: + cmdlist = self._toCmdList_withDelay_m1(volume, speed, player, progressbar) + else: + return (False, f'无法找到算法ID {method} 对应的转换算法') + + if not os.path.exists(self.outputPath): + os.makedirs(self.outputPath) + + with open(f"{self.outputPath}/{self.midFileName}.bdx", "w+") as f: + f.write("BD@") + + _bytes = ( + b"BDX\x00" + + author.encode("utf-8") + + b" & Musicreater\x00\x01command_block\x00" + ) + + key = { + "x": (b"\x0f", b"\x0e"), + "y": (b"\x11", b"\x10"), + "z": (b"\x13", b"\x12"), + } + '''key存储了方块移动指令的数据,其中可以用key[x|y|z][0|1]来表示xyz的减或增''' + x = 'x' + y = 'y' + z = 'z' + + _sideLength = self.__fillSquareSideLength(len(cmdlist), maxheight) + + yforward = True + zforward = True + + nowy = 0 + nowz = 0 + nowx = 0 + + + for cmd, delay in cmdlist: + _bytes += self.__formCMDblk( + cmd, + (1 if yforward else 0) + if ( + ((nowy != 0) and (not yforward)) + or ((yforward) and (nowy != maxheight)) + ) + else (3 if zforward else 2) + if ( + ((nowz != 0) and (not zforward)) + or ((zforward) and (nowz != _sideLength)) + ) + else 5, + impluse=2, + condition=False, + needRedstone=False, + tickDelay=delay, + 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)] + + with open(f"{self.outputPath}/{self.midFileName}.bdx", "ab+") as f: + f.write(brotli.compress(_bytes + b'XE')) + + return (True, _bytes, (nowx, maxheight, _sideLength))