Musicreater/msctPkgver/main.py

1395 lines
50 KiB
Python
Raw Normal View History

2022-06-07 15:43:14 +00:00
# -*- coding: utf-8 -*-
2022-04-05 15:43:03 +00:00
2022-04-29 03:29:49 +00:00
2022-06-07 15:43:14 +00:00
# 音·创 开发交流群 861684859
# Email EillesWan2006@163.com W-YI_DoctorYI@outlook.com EillesWan@outlook.com
# 版权所有 金羿("Eilles Wan") & 诸葛亮与八卦阵("bgArray") & 鸣凤鸽子("MingFengPigeon")
2022-06-18 17:07:07 +00:00
# 若需使用或借鉴 请依照 Apache 2.0 许可证进行许可
2022-06-07 15:43:14 +00:00
"""
2022-06-07 17:50:14 +00:00
· 库版 (Musicreater Package Version)
是一款免费开源的针对我的世界基岩版的midi音乐转换库
2022-06-07 15:43:14 +00:00
注意除了此源文件以外任何属于此仓库以及此项目的文件均依照Apache许可证进行许可
2022-06-07 17:50:14 +00:00
Musicreater pkgver (Package Version · 库版)
A free open source library used for convert midi file into formats that is suitable for **Minecraft: Bedrock Edition**.
2022-06-07 15:43:14 +00:00
Note! Except for this source file, all the files in this repository and this project are licensed under Apache License 2.0
Copyright 2022 all the developers of Musicreater
Licensed under the Apache License, Version 2.0 (the 'License');
2022-04-29 03:29:49 +00:00
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
2022-04-05 15:43:03 +00:00
2022-04-29 03:29:49 +00:00
Unless required by applicable law or agreed to in writing, software
2022-06-07 15:43:14 +00:00
distributed under the License is distributed on an 'AS IS' BASIS,
2022-04-29 03:29:49 +00:00
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
2022-06-07 15:43:14 +00:00
"""
2022-04-29 03:29:49 +00:00
import os
2022-06-18 18:04:17 +00:00
import mido
import brotli
2022-06-30 04:14:22 +00:00
import json
import uuid
import shutil
2022-07-22 09:06:09 +00:00
import math
2022-04-05 15:43:03 +00:00
2022-10-07 08:49:47 +00:00
from .exceptions import *
2022-04-29 03:34:41 +00:00
2022-04-05 15:43:03 +00:00
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
"""
2022-04-29 03:29:49 +00:00
import zipfile
2022-06-30 04:14:22 +00:00
zipf = zipfile.ZipFile(outFilename, "w", compression)
2022-04-05 15:43:03 +00:00
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()
2022-10-07 11:25:28 +00:00
class SingleNote:
def __init__(self, inst: int, pitch: int, velocity, startTime, lastTime):
'''用于存储单个音符的类
:param inst 乐器编号
:param pitch 音符编号
:param velocity 力度/响度
:param startTime 开始之时(ms)
此处的时间是用从乐曲开始到当前的毫秒数
:param lastTime 音符延续时间(ms)'''
self.inst = inst
self.note = pitch
self.velocity = velocity
self.startTime = startTime
self.lastTime = lastTime
@property
def inst(self):
return self.inst
@property
def note(self):
return self.note
@property
def velocity(self):
return self.velocity
@property
def startTime(self):
return self.startTime
@property
def lastTime(self):
return self.lastTime
def __str__(self):
return f"Note(inst = {self.inst}, pitch = {self.note}, velocity = {self.velocity}, 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,
}
'''
学习笔记
tempo: microseconds per quarter note 毫秒每四分音符换句话说就是一拍占多少毫秒
tick: midi帧
ticks_per_beat: 帧每拍即一拍多少帧
那么
tick / ticks_per_beat => amount_of_beats 拍数(四分音符数)
tempo * amount_of_beats => 毫秒数
所以
tempo * tick / ticks_per_beat => 毫秒数
'''
2022-04-05 15:43:03 +00:00
class midiConvert:
2022-10-07 08:49:47 +00:00
def __init__(self, debug: bool = False):
2022-06-30 04:14:22 +00:00
"""简单的midi转换类将midi文件转换为我的世界结构或者包"""
2022-10-07 08:49:47 +00:00
self.debugMode = debug
2022-10-07 11:25:28 +00:00
self.methods = [
self._toCmdList_m1,
self._toCmdList_m2,
self._toCmdList_m3,
]
self.methods_byDelay = [
self._toCmdList_withDelay_m1,
]
def convert(self, midiFile: str, outputPath: str):
2022-06-30 04:14:22 +00:00
"""转换前需要先运行此函数来获取基本信息"""
2022-04-29 03:29:49 +00:00
2022-04-05 15:43:03 +00:00
self.midiFile = midiFile
2022-06-30 04:14:22 +00:00
"""midi文件路径"""
2022-04-05 15:43:03 +00:00
self.midi = mido.MidiFile(self.midiFile)
2022-06-30 04:14:22 +00:00
"""MidiFile对象"""
2022-04-05 15:43:03 +00:00
self.outputPath = outputPath
2022-06-30 04:14:22 +00:00
"""输出路径"""
2022-04-05 15:43:03 +00:00
# 将self.midiFile的文件名不含路径且不含后缀存入self.midiFileName
self.midFileName = os.path.splitext(os.path.basename(self.midiFile))[0]
2022-06-30 04:14:22 +00:00
"""文件名,不含路径且不含后缀"""
2022-04-05 15:43:03 +00:00
2022-06-24 05:46:46 +00:00
def __Inst2soundIDwithX(self, instrumentID):
"""返回midi的乐器ID对应的我的世界乐器名对于音域转换算法如下
2**( ( msg.note - 60 - X ) / 12 ) 即为MC的音高其中
X的取值随乐器不同而变化
竖琴harp电钢琴pling班卓琴banjo方波bit颤音琴iron_xylophone 的时候为6
吉他的时候为7
贝斯bass迪吉里杜管didgeridoo的时候为8
长笛flute牛铃cou_bell的时候为5
钟琴bell管钟chime木琴xylophone的时候为4
2022-06-30 04:14:22 +00:00
而存在一些打击乐器basedrumhatsnare没有音域则没有X那么我们返回7即可
2022-04-05 15:43:03 +00:00
:param instrumentID: midi的乐器ID
2022-10-05 14:18:03 +00:00
default: 如果instrumentID不在范围内返回的默认我的世界乐器名称
2022-06-24 05:46:46 +00:00
:return: (str我的世界乐器名, int转换算法中的X)"""
2022-07-22 09:06:09 +00:00
try:
a = {
0: ("note.harp", 6),
1: ("note.harp", 6),
2: ("note.pling", 6),
3: ("note.harp", 6),
4: ("note.pling", 6),
5: ("note.pling", 6),
6: ("note.harp", 6),
7: ("note.harp", 6),
8: ("note.share", 7), # 打击乐器无音域
9: ("note.harp", 6),
10: ("note.didgeridoo", 8),
11: ("note.harp", 6),
12: ("note.xylophone", 4),
13: ("note.chime", 4),
14: ("note.harp", 6),
15: ("note.harp", 6),
16: ("note.bass", 8),
17: ("note.harp", 6),
18: ("note.harp", 6),
19: ("note.harp", 6),
20: ("note.harp", 6),
21: ("note.harp", 6),
22: ("note.harp", 6),
23: ("note.guitar", 7),
24: ("note.guitar", 7),
25: ("note.guitar", 7),
26: ("note.guitar", 7),
27: ("note.guitar", 7),
28: ("note.guitar", 7),
29: ("note.guitar", 7),
30: ("note.guitar", 7),
31: ("note.bass", 8),
32: ("note.bass", 8),
33: ("note.bass", 8),
34: ("note.bass", 8),
35: ("note.bass", 8),
36: ("note.bass", 8),
37: ("note.bass", 8),
38: ("note.bass", 8),
39: ("note.bass", 8),
40: ("note.harp", 6),
41: ("note.harp", 6),
42: ("note.harp", 6),
43: ("note.harp", 6),
44: ("note.iron_xylophone", 6),
45: ("note.guitar", 7),
46: ("note.harp", 6),
47: ("note.harp", 6),
48: ("note.guitar", 7),
49: ("note.guitar", 7),
50: ("note.bit", 6),
51: ("note.bit", 6),
52: ("note.harp", 6),
53: ("note.harp", 6),
54: ("note.bit", 6),
55: ("note.flute", 5),
56: ("note.flute", 5),
57: ("note.flute", 5),
58: ("note.flute", 5),
59: ("note.flute", 5),
60: ("note.flute", 5),
61: ("note.flute", 5),
62: ("note.flute", 5),
63: ("note.flute", 5),
64: ("note.bit", 6),
65: ("note.bit", 6),
66: ("note.bit", 6),
67: ("note.bit", 6),
68: ("note.flute", 5),
69: ("note.harp", 6),
70: ("note.harp", 6),
71: ("note.flute", 5),
72: ("note.flute", 5),
73: ("note.flute", 5),
74: ("note.harp", 6),
75: ("note.flute", 5),
76: ("note.harp", 6),
77: ("note.harp", 6),
78: ("note.harp", 6),
79: ("note.harp", 6),
80: ("note.bit", 6),
81: ("note.bit", 6),
82: ("note.bit", 6),
83: ("note.bit", 6),
84: ("note.bit", 6),
85: ("note.bit", 6),
86: ("note.bit", 6),
87: ("note.bit", 6),
88: ("note.bit", 6),
89: ("note.bit", 6),
90: ("note.bit", 6),
91: ("note.bit", 6),
92: ("note.bit", 6),
93: ("note.bit", 6),
94: ("note.bit", 6),
95: ("note.bit", 6),
96: ("note.bit", 6),
97: ("note.bit", 6),
98: ("note.bit", 6),
99: ("note.bit", 6),
100: ("note.bit", 6),
101: ("note.bit", 6),
102: ("note.bit", 6),
103: ("note.bit", 6),
104: ("note.harp", 6),
105: ("note.banjo", 6),
106: ("note.harp", 6),
107: ("note.harp", 6),
108: ("note.harp", 6),
109: ("note.harp", 6),
110: ("note.harp", 6),
111: ("note.guitar", 7),
112: ("note.harp", 6),
113: ("note.bell", 4),
114: ("note.harp", 6),
115: ("note.cow_bell", 5),
116: ("note.basedrum", 7), # 打击乐器无音域
117: ("note.bass", 8),
118: ("note.bit", 6),
119: ("note.basedrum", 7), # 打击乐器无音域
120: ("note.guitar", 7),
121: ("note.harp", 6),
122: ("note.harp", 6),
123: ("note.harp", 6),
124: ("note.harp", 6),
125: ("note.hat", 7), # 打击乐器无音域
126: ("note.basedrum", 7), # 打击乐器无音域
127: ("note.snare", 7), # 打击乐器无音域
}[instrumentID]
2022-10-05 14:18:03 +00:00
except BaseException:
2022-07-22 09:06:09 +00:00
a = ("note.harp", 6)
return a
2022-04-05 15:43:03 +00:00
def __score2time(self, score: int):
2022-10-07 08:49:47 +00:00
return str(int(int(score / 20) / 60)) + ":" + str(int(int(score / 20) % 60))
def __formProgressBar(
2022-10-07 08:49:47 +00:00
self,
maxscore: int,
scoreboardname: str,
progressbar: tuple = (
r"%%N [ %%s/%^s %%% __________ %%t|%^t ]",
("§e=§r", "§7=§r"),
),
) -> list:
2022-06-18 17:07:07 +00:00
pgsstyle = progressbar[0]
2022-06-30 04:14:22 +00:00
"""用于被替换的进度条原始样式"""
2022-06-30 04:14:22 +00:00
"""
| 标识符 | 指定的可变量 |
|---------|----------------|
| `%%N` | 乐曲名(即传入的文件名)|
| `%%s` | 当前计分板值 |
| `%^s` | 计分板最大值 |
| `%%t` | 当前播放时间 |
| `%^t` | 曲目总时长 |
| `%%%` | 当前进度比率 |
| `_` | 用以表示进度条占位|
2022-06-30 04:14:22 +00:00
"""
def __replace(
2022-10-07 08:49:47 +00:00
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)
2022-06-30 04:14:22 +00:00
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 = {
2022-06-30 04:14:22 +00:00
r"%%N": self.midFileName,
r"%%s": r"%%s",
r"%^s": str(maxscore),
r"%%t": r"%%t",
r"%^t": self.__score2time(maxscore),
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
2022-06-30 04:14:22 +00:00
pgblength = pgsstyle.count("_")
"""进度条的“条”长度"""
finalprgsbar = []
for i in range(maxscore):
nowstr = pgsstyle
2022-10-05 14:18:03 +00:00
if ids[r"%%s"]:
2022-06-30 04:14:22 +00:00
nowstr = nowstr.replace(r"%%s", str(i + 1))
2022-10-05 14:18:03 +00:00
if ids[r"%%t"]:
2022-06-30 04:14:22 +00:00
nowstr = nowstr.replace(r"%%t", self.__score2time(i + 1))
2022-10-05 14:18:03 +00:00
if ids[r"%%%"]:
nowstr = nowstr.replace(
2022-06-30 04:14:22 +00:00
r"%%%", str(int((i + 1) / maxscore * 10000) / 100) + "%"
)
countof_s = int((i + 1) / maxscore * pgblength)
finalprgsbar.append(
2022-10-07 08:49:47 +00:00
"title @a[scores={"
+ scoreboardname
+ "="
+ str(i + 1)
+ "}] actionbar "
+ __replace(
nowstr, "_", progressbar[1][0], countof_s, progressbar[1][1]
)
)
return finalprgsbar
def __formCMDblk(
2022-10-07 08:49:47 +00:00
self,
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`
悬浮字
2022-10-05 14:18:03 +00:00
lastOutput: `str`
上次输出字符串注意此处需要留空
:param executeOnFirstTick: `bool`
执行第一个已选项(循环指令方块是否激活后立即执行若为False则从激活时起延迟后第一次执行)
:param trackOutput: `bool`
是否输出
:return:str
"""
2022-10-07 08:49:47 +00:00
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",
2022-06-30 04:14:22 +00:00
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
2022-04-29 03:29:49 +00:00
def _toCmdList_m1(
2022-10-07 08:49:47 +00:00
self, scoreboardname: str = "mscplay", volume: float = 1.0, speed: float = 1.0
) -> list:
"""
2022-06-18 17:07:07 +00:00
使用Dislink Sforza的转换思路将midi转换为我的世界命令列表
2022-04-05 15:43:03 +00:00
:param scoreboardname: 我的世界的计分板名称
:param volume: 音量注意这里的音量范围为(0,1]如果超出将被处理为正确值其原理为在距离玩家 (1 / volume -1) 的地方播放音频
:param speed: 速度注意这里的速度指的是播放倍率其原理为在播放音频的时候每个音符的播放时间除以 speed
:return: tuple(命令列表, 命令个数, 计分板最大值)
"""
2022-04-05 15:43:03 +00:00
tracks = []
if volume > 1:
volume = 1
if volume <= 0:
volume = 0.001
2022-04-29 03:29:49 +00:00
commands = 0
maxscore = 0
2022-04-29 03:29:49 +00:00
2022-04-05 15:43:03 +00:00
for i, track in enumerate(self.midi.tracks):
2022-04-29 03:29:49 +00:00
ticks = 0
instrumentID = 0
2022-04-05 15:43:03 +00:00
singleTrack = []
for msg in track:
2022-06-18 17:07:07 +00:00
ticks += msg.time
2022-06-25 15:20:56 +00:00
# print(msg)
2022-04-05 15:43:03 +00:00
if msg.is_meta:
2022-06-30 04:14:22 +00:00
if msg.type == "set_tempo":
2022-04-29 03:29:49 +00:00
tempo = msg.tempo
2022-06-25 15:20:56 +00:00
else:
2022-06-30 04:14:22 +00:00
if msg.type == "program_change":
2022-06-25 15:20:56 +00:00
# print("TT")
2022-04-29 03:29:49 +00:00
instrumentID = msg.program
2022-06-30 04:14:22 +00:00
if msg.type == "note_on" and msg.velocity != 0:
2022-10-07 08:49:47 +00:00
try:
nowscore = round(
(ticks * tempo)
/ ((self.midi.ticks_per_beat * float(speed)) * 50000)
)
except NameError:
2022-10-07 11:25:28 +00:00
raise NotDefineTempoError('计算当前分数时出错 未定义参量 Tempo')
maxscore = max(maxscore, nowscore)
2022-06-24 05:46:46 +00:00
soundID, _X = self.__Inst2soundIDwithX(instrumentID)
2022-04-29 03:29:49 +00:00
singleTrack.append(
2022-10-07 08:49:47 +00:00
"execute @a[scores={"
+ str(scoreboardname)
+ "="
+ str(nowscore)
+ "}"
+ f"] ~ ~ ~ playsound {soundID} @s ~ ~{1 / volume - 1} ~ {msg.velocity * (0.7 if msg.channel == 0 else 0.9)} {2 ** ((msg.note - 60 - _X) / 12)}"
)
2022-04-29 03:29:49 +00:00
commands += 1
if len(singleTrack) != 0:
tracks.append(singleTrack)
2022-10-05 14:18:03 +00:00
return [tracks, commands, maxscore]
2022-10-07 11:25:28 +00:00
# 这与上面的算法几乎没有差别 甚至更慢了一点 但是是为了线性插值做准备
2022-08-08 14:08:08 +00:00
def _toCmdList_m2(
2022-10-07 11:25:28 +00:00
self,
scoreboardname: str = "mscplay",
MaxVolume: float = 1.0,
speed: float = 1.0,
2022-10-07 08:49:47 +00:00
) -> list:
2022-08-08 14:08:08 +00:00
"""
2022-10-07 11:25:28 +00:00
使用金羿的转换思路将midi转换为我的世界命令列表
2022-08-08 14:08:08 +00:00
:param scoreboardname: 我的世界的计分板名称
2022-10-07 11:25:28 +00:00
:param MaxVolume: 音量注意这里的音量范围为(0,1]如果超出将被处理为正确值其原理为在距离玩家 (1 / volume -1) 的地方播放音频
2022-08-08 14:08:08 +00:00
:param speed: 速度注意这里的速度指的是播放倍率其原理为在播放音频的时候每个音符的播放时间除以 speed
:return: tuple(命令列表, 命令个数, 计分板最大值)
"""
2022-10-07 11:25:28 +00:00
2022-10-07 08:49:47 +00:00
if MaxVolume > 1:
MaxVolume = 1
if MaxVolume <= 0:
MaxVolume = 0.001
2022-08-08 14:08:08 +00:00
2022-10-07 11:25:28 +00:00
# 一个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:
try:
msg.channel
channelMsg = True
except:
channelMsg = False
if channelMsg:
if msg.channel > 15:
raise ChannelOverFlowError(f"当前消息 {msg} 的通道超限(≤15)")
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
CheckFirstChannel = False
# 此处 我们把通道视为音轨
for track in channels:
# 如果当前通道为空 则跳过
if not track:
continue
if channels.index(track) == 0:
CheckFirstChannel = True
else:
CheckFirstChannel = False
nowTrack = []
for msg in track:
if msg[0] == "PgmC":
InstID = msg[1]
elif msg[0] == "NoteS":
soundID, _X = self.__Inst2soundIDwithX(InstID)
score_now = round(msg[-1]/ float(speed) / 50000)
maxScore = max(maxScore,score_now)
nowTrack.append(
"execute @a[scores={"
+ str(scoreboardname)
+ "="
+ str(score_now)
+ "}"
+ f"] ~ ~ ~ playsound {soundID} @s ~ ~{1 / MaxVolume - 1} ~ {msg[2] * (0.7 if CheckFirstChannel else 0.9)} {2 ** ((msg[1] - 60 - _X) / 12)}"
)
cmdAmount += 1
tracks.append(nowTrack)
return [tracks, cmdAmount, maxScore]
# 这才是加了线性插值的算法
def _toCmdList_m3(
self,
scoreboardname: str = "mscplay",
MaxVolume: float = 1.0,
speed: float = 1.0,
) -> list:
"""
使用金羿的转换思路将midi转换为我的世界命令列表并使用线性插值算法优化音量此处的插值是固定斜率的方法
:param scoreboardname: 我的世界的计分板名称
:param MaxVolume: 音量注意这里的音量范围为(0,1]如果超出将被处理为正确值其原理为在距离玩家 (1 / volume -1) 的地方播放音频
:param speed: 速度注意这里的速度指的是播放倍率其原理为在播放音频的时候每个音符的播放时间除以 speed
:return: tuple(命令列表, 命令个数, 计分板最大值)
"""
if MaxVolume > 1:
MaxVolume = 1
if MaxVolume <= 0:
MaxVolume = 0.001
2022-08-08 14:08:08 +00:00
2022-10-07 08:49:47 +00:00
# 一个midi中仅有16通道 我们通过通道来识别而不是音轨
channels = [[], [], [], [], [], [], [], [], [], [], [], [], [], [], [], []]
2022-10-07 11:25:28 +00:00
# 我们来用通道统计音乐信息
2022-08-08 14:08:08 +00:00
for i, track in enumerate(self.midi.tracks):
2022-10-07 11:25:28 +00:00
microseconds = 0
2022-08-08 14:08:08 +00:00
for msg in track:
2022-10-07 11:25:28 +00:00
if msg.time != 0:
try:
microseconds += msg.time * tempo / self.midi.ticks_per_beat
except NameError:
raise NotDefineTempoError('计算当前分数时出错 未定义参量 Tempo')
2022-08-08 14:08:08 +00:00
if msg.is_meta:
if msg.type == "set_tempo":
tempo = msg.tempo
else:
2022-10-07 11:25:28 +00:00
try:
msg.channel
channelMsg = True
except:
channelMsg = False
if channelMsg:
if msg.channel > 15:
raise ChannelOverFlowError(f"当前消息 {msg} 的通道超限(≤15)")
2022-08-08 14:08:08 +00:00
if msg.type == "program_change":
2022-10-07 11:25:28 +00:00
channels[msg.channel].append(
("PgmC", msg.program, microseconds)
2022-08-08 14:08:08 +00:00
)
2022-10-07 11:25:28 +00:00
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)
2022-10-07 08:49:47 +00:00
)
2022-08-08 14:08:08 +00:00
2022-10-07 11:25:28 +00:00
'''整合后的音乐通道格式
每个通道包括若干消息元素其中逃不过这三种
1 切换乐器消息
("PgmC", 切换后的乐器ID: int, 距离演奏开始的毫秒)
2 音符开始消息
("NoteS", 开始的音符ID, 力度响度, 距离演奏开始的毫秒)
3 音符结束消息
("NoteS", 结束的音符ID, 距离演奏开始的毫秒)'''
note_channels = [[], [], [], [], [], [], [], [], [], [], [], [], [], [], [], []]
# 此处 我们把通道视为音轨
for i in range(len(channels)):
# 如果当前通道为空 则跳过
noteMsgs = []
for msg in channels[i]:
if msg[0] == "PgmC":
InstID = msg[1]
elif msg[0] == "NoteS":
noteMsgs.append(msg[1:])
elif msg[0] == "NoteE":
record_pop_ids = []
for j in range(len(noteMsgs)):
if noteMsgs[j][0] == msg[1]:
note_channels[i].append(SingleNote(InstID, msg[1],noteMsgs[j][1],noteMsgs[j][2],msg[-1]-noteMsgs[j][2]))
record_pop_ids.append(j)
for j in record_pop_ids:
noteMsgs.pop(j)
tracks = []
cmdAmount = 0
maxScore = 0
CheckFirstChannel = False
# 此处 我们把通道视为音轨
for track in note_channels:
# 如果当前通道为空 则跳过
if not track:
continue
if channels.index(track) == 0:
CheckFirstChannel = True
else:
CheckFirstChannel = False
nowTrack = []
for note in track:
for i in range(note.lastTime / 500):
# 实在没时间写了
# 用半秒做一个时间单位
# 然后计算即可
# 我找时间写完
soundID, _X = self.__Inst2soundIDwithX(InstID)
score_now = round(msg[-1]/ float(speed) / 50000)
maxScore = max(maxScore,score_now)
nowTrack.append(
"execute @a[scores={"
+ str(scoreboardname)
+ "="
+ str(score_now)
+ "}"
+ f"] ~ ~ ~ playsound {soundID} @s ~ ~{1 / MaxVolume - 1} ~ {msg[2] * (0.7 if CheckFirstChannel else 0.9)} {2 ** ((msg[1] - 60 - _X) / 12)}"
)
cmdAmount += 1
tracks.append(nowTrack)
return [tracks, cmdAmount, maxScore]
return [tracks, cmdAmount, maxScore]
2022-08-08 14:08:08 +00:00
2022-06-18 17:07:07 +00:00
def _toCmdList_withDelay_m1(
2022-10-07 08:49:47 +00:00
self,
volume: float = 1.0,
speed: float = 1.0,
player: str = "@a",
isMixedWithPrograssBar=False,
2022-06-18 17:07:07 +00:00
) -> list:
"""
2022-06-18 17:07:07 +00:00
使用Dislink Sforza的转换思路将midi转换为我的世界命令列表并输出每个音符之后的延迟
:param volume: 音量注意这里的音量范围为(0,1]如果超出将被处理为正确值其原理为在距离玩家 (1 / volume -1) 的地方播放音频
:param speed: 速度注意这里的速度指的是播放倍率其原理为在播放音频的时候每个音符的播放时间除以 speed
2022-06-18 17:07:07 +00:00
:param player: 玩家选择器默认为`@a`
:param isMixedWithPrograssBar: 进度条当此参数为True时使用默认进度条当此参数为其他值为真的表达式时识别为进度条自定义参数若为其他值为假的表达式则不生成进度条
:return: 全部指令列表[ ( str指令, int距离上一个指令的延迟 ),...]
"""
2022-06-18 17:07:07 +00:00
tracks = {}
if volume > 1:
volume = 1
if volume <= 0:
volume = 0.001
2022-10-07 08:49:47 +00:00
# 此处是对于仅有 True 的参数和自定义参数的判断
if isMixedWithPrograssBar == True:
2022-06-18 17:26:53 +00:00
isMixedWithPrograssBar = (
2022-06-30 04:14:22 +00:00
r"%%N [ %%s/%^s %%% __________ %%t|%^t ]",
("§e=§r", "§7=§r"),
2022-06-18 17:26:53 +00:00
)
for i, track in enumerate(self.midi.tracks):
instrumentID = 0
2022-06-18 17:07:07 +00:00
ticks = 0
for msg in track:
2022-06-18 17:07:07 +00:00
ticks += msg.time
if msg.is_meta:
2022-06-30 04:14:22 +00:00
if msg.type == "set_tempo":
tempo = msg.tempo
else:
2022-06-30 04:14:22 +00:00
if msg.type == "program_change":
instrumentID = msg.program
2022-06-30 04:14:22 +00:00
if msg.type == "note_on" and msg.velocity != 0:
2022-06-18 17:07:07 +00:00
nowtick = round(
(ticks * tempo)
/ ((self.midi.ticks_per_beat * float(speed)) * 50000)
)
2022-06-24 05:46:46 +00:00
soundID, _X = self.__Inst2soundIDwithX(instrumentID)
2022-06-18 17:07:07 +00:00
try:
tracks[nowtick].append(
2022-06-30 04:14:22 +00:00
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)}"
2022-06-18 17:07:07 +00:00
)
2022-10-05 14:18:03 +00:00
except BaseException:
2022-06-18 17:07:07 +00:00
tracks[nowtick] = [
2022-10-07 08:49:47 +00:00
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)}",
]
2022-06-18 17:07:07 +00:00
allticks = list(tracks.keys())
if isMixedWithPrograssBar:
pgsstyle = isMixedWithPrograssBar[0]
2022-06-30 04:14:22 +00:00
"""用于被替换的进度条原始样式"""
2022-06-18 17:07:07 +00:00
2022-06-30 04:14:22 +00:00
"""
2022-06-18 17:07:07 +00:00
| 标识符 | 指定的可变量 |
|---------|----------------|
| `%%N` | 乐曲名(即传入的文件名)|
| `%%s` | 当前计分板值 |
| `%^s` | 计分板最大值 |
| `%%t` | 当前播放时间 |
| `%^t` | 曲目总时长 |
| `%%%` | 当前进度比率 |
| `_` | 用以表示进度条占位|
2022-06-30 04:14:22 +00:00
"""
2022-06-18 17:07:07 +00:00
def __replace(
2022-10-07 08:49:47 +00:00
s: str, tobeReplaced: str, replaceWith: str, times: int, other: str
):
2022-06-18 17:07:07 +00:00
if times == 0:
return s.replace(tobeReplaced, other)
if times == s.count(tobeReplaced):
return s.replace(tobeReplaced, replaceWith)
2022-06-30 04:14:22 +00:00
result = ""
2022-06-18 17:07:07 +00:00
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 = {
2022-06-30 04:14:22 +00:00
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"%%%",
2022-06-18 17:07:07 +00:00
}
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
2022-06-30 04:14:22 +00:00
pgblength = pgsstyle.count("_")
"""进度条的“条”长度"""
2022-06-18 17:07:07 +00:00
results = []
for i in range(len(allticks)):
if i != 0:
for j in range(len(tracks[allticks[i]])):
2022-06-18 17:26:53 +00:00
if j != 0:
2022-06-18 17:07:07 +00:00
results.append((tracks[allticks[i]][j], 0))
else:
results.append(
(tracks[allticks[i]][j], allticks[i] - allticks[i - 1])
)
2022-06-18 17:07:07 +00:00
else:
for j in range(len(tracks[allticks[i]])):
results.append((tracks[allticks[i]][j], allticks[i]))
if isMixedWithPrograssBar:
nowstr = pgsstyle
2022-10-05 14:18:03 +00:00
if ids[r"%%s"]:
2022-06-30 04:14:22 +00:00
nowstr = nowstr.replace(r"%%s", str(allticks[i] + 1))
2022-10-05 14:18:03 +00:00
if ids[r"%%t"]:
2022-10-07 08:49:47 +00:00
nowstr = nowstr.replace(r"%%t", self.__score2time(allticks[i] + 1))
2022-10-05 14:18:03 +00:00
if ids[r"%%%"]:
2022-10-07 08:49:47 +00:00
nowstr = nowstr.replace(
r"%%%",
str(int((allticks[i] + 1) / allticks[-1] * 10000) / 100) + "%",
)
2022-04-05 15:43:03 +00:00
2022-06-18 17:36:52 +00:00
countof_s = int((allticks[i] + 1) / allticks[-1] * pgblength)
2022-04-29 03:29:49 +00:00
2022-06-18 17:07:07 +00:00
titlenow = __replace(
nowstr,
2022-06-30 04:14:22 +00:00
"_",
2022-06-18 17:07:07 +00:00
isMixedWithPrograssBar[1][0],
countof_s,
isMixedWithPrograssBar[1][1],
)
2022-06-30 04:14:22 +00:00
results.append(
(
f"title {player} actionbar {titlenow}",
0,
)
)
2022-06-18 17:07:07 +00:00
return results
def __fillSquareSideLength(self, total: int, maxHeight: int):
2022-06-30 04:14:22 +00:00
"""给定总方块数量和最大高度,返回所构成的图形外切正方形的边长
:param total: 总方块数量
:param maxHeight: 最大高度
2022-06-30 04:14:22 +00:00
:return: 外切正方形的边长 int"""
return math.ceil(math.sqrt(math.ceil(total / maxHeight)))
2022-04-29 03:29:49 +00:00
def tomcpack(
2022-10-07 08:49:47 +00:00
self,
method: int = 1,
isAutoReset: bool = False,
progressbar=None,
scoreboardname: str = "mscplay",
volume: float = 1.0,
speed: float = 1.0,
2022-10-05 14:18:03 +00:00
) -> bool or tuple:
"""
使用method指定的转换算法将midi转换为我的世界mcpack格式的包
2022-04-05 15:43:03 +00:00
:param method: 转换算法
:param isAutoReset: 是否自动重置计分板
:param progressbar: 进度条当此参数为True时使用默认进度条为其他的值为真的参数时识别为进度条自定义参数为其他值为假的时候不生成进度条
2022-04-05 15:43:03 +00:00
:param scoreboardname: 我的世界的计分板名称
:param volume: 音量注意这里的音量范围为(0,1]其原理为在距离玩家 (1 / volume -1) 的地方播放音频
:param speed: 速度注意这里的速度指的是播放倍率其原理为在播放音频的时候每个音符的播放时间除以 speed
:return 成功与否成功返回(True,True)失败返回(False,str失败原因)
"""
2022-10-07 11:25:28 +00:00
try:
cmdlist, _a, maxscore = self.methods[method-1](scoreboardname, volume, speed)
except:
2022-06-30 04:14:22 +00:00
return (False, f"无法找到算法ID{method}对应的转换算法")
2022-04-29 03:29:49 +00:00
del _a
2022-04-05 15:43:03 +00:00
# 当文件f夹{self.outputPath}/temp/functions存在时清空其下所有项目若其不存在则创建
2022-06-30 04:14:22 +00:00
if os.path.exists(f"{self.outputPath}/temp/functions/"):
shutil.rmtree(f"{self.outputPath}/temp/functions/")
os.makedirs(f"{self.outputPath}/temp/functions/mscplay")
2022-04-05 15:43:03 +00:00
# 写入manifest.json
2022-06-30 04:14:22 +00:00
if not os.path.exists(f"{self.outputPath}/temp/manifest.json"):
with open(
2022-10-07 08:49:47 +00:00
f"{self.outputPath}/temp/manifest.json", "w", encoding="utf-8"
) as f:
2022-10-07 08:49:47 +00:00
f.write(
'{\n "format_version": 1,\n "header": {\n "description": "'
+ self.midFileName
+ ' Pack : behavior pack",\n "version": [ 0, 0, 1 ],\n "name": "'
+ self.midFileName
+ 'Pack",\n "uuid": "'
+ str(uuid.uuid4())
+ '"\n },\n "modules": [\n {\n "description": "'
+ f"the Player of the Music {self.midFileName}"
+ '",\n "type": "data",\n "version": [ 0, 0, 1 ],\n "uuid": "'
+ str(uuid.uuid4())
+ '"\n }\n ]\n}'
)
2022-04-05 15:43:03 +00:00
else:
with open(
2022-10-07 08:49:47 +00:00
f"{self.outputPath}/temp/manifest.json", "r", encoding="utf-8"
) as manifest:
2022-04-29 03:29:49 +00:00
data = json.loads(manifest.read())
2022-06-30 04:14:22 +00:00
data["header"][
"description"
2022-04-29 03:29:49 +00:00
] = f"the Player of the Music {self.midFileName}"
2022-06-30 04:14:22 +00:00
data["header"]["name"] = self.midFileName
data["header"]["uuid"] = str(uuid.uuid4())
data["modules"][0]["description"] = "None"
data["modules"][0]["uuid"] = str(uuid.uuid4())
2022-04-29 03:29:49 +00:00
manifest.close()
2022-10-07 08:49:47 +00:00
open(f"{self.outputPath}/temp/manifest.json", "w", encoding="utf-8").write(
json.dumps(data)
)
2022-04-05 15:43:03 +00:00
# 将命令列表写入文件
2022-04-29 03:29:49 +00:00
indexfile = open(
2022-10-07 08:49:47 +00:00
f"{self.outputPath}/temp/functions/index.mcfunction", "w", encoding="utf-8"
)
2022-04-05 15:43:03 +00:00
for track in cmdlist:
2022-04-29 03:29:49 +00:00
indexfile.write(
2022-06-30 04:14:22 +00:00
"function mscplay/track" + str(cmdlist.index(track) + 1) + "\n"
2022-04-29 03:29:49 +00:00
)
with open(
2022-10-07 08:49:47 +00:00
f"{self.outputPath}/temp/functions/mscplay/track{cmdlist.index(track) + 1}.mcfunction",
"w",
encoding="utf-8",
2022-04-29 03:29:49 +00:00
) as f:
2022-06-30 04:14:22 +00:00
f.write("\n".join(track))
indexfile.writelines(
(
2022-06-30 04:14:22 +00:00
"scoreboard players add @a[scores={"
+ scoreboardname
2022-06-30 04:14:22 +00:00
+ "=1..}] "
+ scoreboardname
2022-06-30 04:14:22 +00:00
+ " 1\n",
(
2022-06-30 04:14:22 +00:00
"scoreboard players reset @a[scores={"
+ scoreboardname
+ "="
+ str(maxscore + 20)
+ "..}]"
+ f" {scoreboardname}\n"
)
if isAutoReset
2022-06-30 04:14:22 +00:00
else "",
f"function mscplay/progressShow\n" if progressbar else "",
)
2022-04-29 03:29:49 +00:00
)
if progressbar:
2022-10-05 14:18:03 +00:00
if progressbar:
with open(
2022-10-07 08:49:47 +00:00
f"{self.outputPath}/temp/functions/mscplay/progressShow.mcfunction",
"w",
encoding="utf-8",
) as f:
f.writelines(
2022-10-07 08:49:47 +00:00
"\n".join(self.__formProgressBar(maxscore, scoreboardname))
)
else:
with open(
2022-10-07 08:49:47 +00:00
f"{self.outputPath}/temp/functions/mscplay/progressShow.mcfunction",
"w",
encoding="utf-8",
) as f:
f.writelines(
2022-06-30 04:14:22 +00:00
"\n".join(
self.__formProgressBar(
maxscore, scoreboardname, progressbar
)
)
)
2022-04-05 15:43:03 +00:00
indexfile.close()
2022-04-29 03:29:49 +00:00
makeZip(
2022-10-07 08:49:47 +00:00
f"{self.outputPath}/temp/", self.outputPath + f"/{self.midFileName}.mcpack"
)
2022-04-05 15:43:03 +00:00
2022-06-30 04:14:22 +00:00
shutil.rmtree(f"{self.outputPath}/temp/")
2022-04-05 15:43:03 +00:00
2022-04-29 03:29:49 +00:00
def toBDXfile(
2022-10-07 08:49:47 +00:00
self,
method: int = 1,
author: str = "Eilles",
progressbar=False,
maxheight: int = 64,
scoreboardname: str = "mscplay",
volume: float = 1.0,
speed: float = 1.0,
isAutoReset: bool = False,
):
"""
使用method指定的转换算法将midi转换为BDX结构文件
2022-04-29 03:29:49 +00:00
:param method: 转换算法
:param author: 作者名称
:param progressbar: 进度条当此参数为True时使用默认进度条为其他的值为真的参数时识别为进度条自定义参数为其他值为假的时候不生成进度条
:param maxheight: 生成结构最大高度
2022-04-29 03:29:49 +00:00
:param scoreboardname: 我的世界的计分板名称
:param volume: 音量注意这里的音量范围为(0,1]如果超出将被处理为正确值其原理为在距离玩家 (1 / volume -1) 的地方播放音频
:param speed: 速度注意这里的速度指的是播放倍率其原理为在播放音频的时候每个音符的播放时间除以 speed
:param isAutoReset: 是否自动重置计分板
:return 成功与否成功返回(True,未经过压缩的源,结构占用大小)失败返回(False,str失败原因)
"""
2022-10-07 11:25:28 +00:00
try:
cmdlist, totalcount, maxScore = self.methods[method-1](scoreboardname, volume, speed)
except:
return (False, f"无法找到算法ID{method}对应的转换算法")
2022-04-29 03:29:49 +00:00
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 = (
2022-06-30 04:14:22 +00:00
b"BDX\x00"
+ author.encode("utf-8")
+ b" & Musicreater\x00\x01command_block\x00"
2022-04-29 03:29:49 +00:00
)
key = {
"x": (b"\x0f", b"\x0e"),
"y": (b"\x11", b"\x10"),
"z": (b"\x13", b"\x12"),
}
2022-06-30 04:14:22 +00:00
"""key存储了方块移动指令的数据其中可以用key[x|y|z][0|1]来表示xyz的减或增"""
x = "x"
y = "y"
z = "z"
2022-04-29 03:29:49 +00:00
_sideLength = self.__fillSquareSideLength(totalcount, maxheight)
2022-04-29 03:29:49 +00:00
yforward = True
zforward = True
nowy = 0
nowz = 0
nowx = 0
2022-04-29 03:29:49 +00:00
commands = []
2022-04-29 03:29:49 +00:00
for track in cmdlist:
commands += track
if isAutoReset:
commands += (
2022-06-30 04:14:22 +00:00
"scoreboard players reset @a[scores={"
+ scoreboardname
+ "="
+ str(maxScore + 20)
+ "}] "
+ scoreboardname
)
2022-10-07 08:49:47 +00:00
# 此处是对于仅有 True 的参数和自定义参数的判断
if progressbar:
2022-10-07 08:49:47 +00:00
if progressbar == True:
commands += self.__formProgressBar(maxScore, scoreboardname)
else:
commands += self.__formProgressBar(
maxScore, scoreboardname, progressbar
)
for cmd in commands:
_bytes += self.__formCMDblk(
cmd,
(1 if yforward else 0)
if (
2022-06-30 04:14:22 +00:00
((nowy != 0) and (not yforward))
or ((yforward) and (nowy != maxheight))
)
else (3 if zforward else 2)
if (
2022-06-30 04:14:22 +00:00
((nowz != 0) and (not zforward))
or ((zforward) and (nowz != _sideLength))
)
else 5,
impluse=2,
condition=False,
needRedstone=False,
tickDelay=0,
2022-06-30 04:14:22 +00:00
customName="",
executeOnFirstTick=False,
trackOutput=True,
)
nowy += 1 if yforward else -1
2022-10-07 08:49:47 +00:00
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 (
2022-10-07 08:49:47 +00:00
(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)]
2022-04-29 03:29:49 +00:00
with open(f"{self.outputPath}/{self.midFileName}.bdx", "ab+") as f:
2022-06-30 04:14:22 +00:00
f.write(brotli.compress(_bytes + b"XE"))
2022-04-29 03:29:49 +00:00
return (True, _bytes, (nowx, maxheight, _sideLength))
2022-06-18 17:07:07 +00:00
def toBDXfile_withDelay(
2022-10-07 08:49:47 +00:00
self,
method: int = 1,
author: str = "Eilles",
progressbar=False,
maxheight: int = 64,
volume: float = 1.0,
speed: float = 1.0,
player: str = "@a",
2022-06-18 17:07:07 +00:00
):
"""
使用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失败原因)
"""
2022-10-07 11:25:28 +00:00
try:
cmdlist = self.methods_byDelay[method-1](volume, speed, player, progressbar)
except:
return (False, f"无法找到算法ID{method}对应的转换算法")
2022-06-18 17:07:07 +00:00
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 = (
2022-06-30 04:14:22 +00:00
b"BDX\x00"
+ author.encode("utf-8")
+ b" & Musicreater\x00\x01command_block\x00"
2022-06-18 17:07:07 +00:00
)
key = {
"x": (b"\x0f", b"\x0e"),
"y": (b"\x11", b"\x10"),
"z": (b"\x13", b"\x12"),
}
2022-06-30 04:14:22 +00:00
"""key存储了方块移动指令的数据其中可以用key[x|y|z][0|1]来表示xyz的减或增"""
x = "x"
y = "y"
z = "z"
2022-06-18 17:07:07 +00:00
_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 (
2022-06-30 04:14:22 +00:00
((nowy != 0) and (not yforward))
or ((yforward) and (nowy != maxheight))
2022-06-18 17:07:07 +00:00
)
else (3 if zforward else 2)
if (
2022-06-30 04:14:22 +00:00
((nowz != 0) and (not zforward))
or ((zforward) and (nowz != _sideLength))
2022-06-18 17:07:07 +00:00
)
else 5,
impluse=2,
condition=False,
needRedstone=False,
tickDelay=delay,
2022-06-30 04:14:22 +00:00
customName="",
2022-06-18 17:07:07 +00:00
executeOnFirstTick=False,
trackOutput=True,
)
nowy += 1 if yforward else -1
2022-10-07 08:49:47 +00:00
if ((nowy > maxheight) and (yforward)) or ((nowy < 0) and (not yforward)):
2022-06-18 17:07:07 +00:00
nowy -= 1 if yforward else -1
yforward = not yforward
nowz += 1 if zforward else -1
if ((nowz > _sideLength) and (zforward)) or (
2022-10-07 08:49:47 +00:00
(nowz < 0) and (not zforward)
2022-06-18 17:07:07 +00:00
):
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:
2022-06-30 04:14:22 +00:00
f.write(brotli.compress(_bytes + b"XE"))
2022-06-18 17:07:07 +00:00
return (True, _bytes, (nowx, maxheight, _sideLength))