mirror of
https://github.com/TriM-Organization/Musicreater.git
synced 2024-11-11 01:27:35 +08:00
新增bdx转换功能
This commit is contained in:
parent
8f6cc04780
commit
1805ab53c0
@ -41,13 +41,10 @@
|
||||
|
||||
## 致谢🙏
|
||||
|
||||
- 感谢由 [Fuckcraft](https://github.com/fuckcraft) “鸣凤鸽子”等 带来的我的世界websocket服务器功能
|
||||
- 感谢 昀梦\<QQ1515399885\> 找出指令生成错误bug并指正
|
||||
- 感谢由 Charlie_Ping “查理平” 带来的bdx转换功能
|
||||
- 感谢由 CMA_2401PT 带来的 BDXWorkShop 供本程序对于bdx操作的指导
|
||||
- 感谢由 Miracle Plume “神羽” \<QQshenyu40403\>带来的羽音缭绕基岩版音色资源包
|
||||
- 感谢由 Dislink Sforza \<QQ1600515314\>带来的midi转换算法,我们将其加入了我们众多算法之一
|
||||
- 感谢 Arthur Morgan 对本程序的排错提出了最大的支持
|
||||
- 感谢由 Charlie_Ping “查理平” 带来的bdx文件转换参考
|
||||
- 感谢由 CMA_2401PT 为我们的软件开发进行指导
|
||||
- 感谢由 Dislink Sforza \<QQ1600515314\>带来的midi音色解析以及转换指令的算法,我们将其加入了我们众多算法之一
|
||||
- 感谢广大群友为此程序提供的测试等支持
|
||||
- 若您对我们有所贡献但您的名字没有显示在此列表中,请联系我!
|
||||
|
||||
|
9
example_convert_bdx.py
Normal file
9
example_convert_bdx.py
Normal file
@ -0,0 +1,9 @@
|
||||
|
||||
|
||||
|
||||
# THIS PROGRAM IS ONLY A TEST EXAMPLE
|
||||
|
||||
|
||||
from main import *
|
||||
|
||||
midiConvert(input('请输入midi文件路径:'), input('请输入输出路径:')).toBDXfile(1,input('请输入作者:'),int(input('请输入指令结构最大生成高度:')),input('请输入计分板名称:'),float(input('请输入音量(0-1]:')),float(input('请输入速度倍率:')))
|
322
main.py
322
main.py
@ -1,13 +1,22 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
import mido
|
||||
import os
|
||||
import json
|
||||
import uuid
|
||||
import zipfile
|
||||
import shutil
|
||||
import zipfile
|
||||
'''
|
||||
Copyright © 2022 Team-Ryoun 金羿("Eilles Wan") & 诸葛亮与八卦阵("bgArray")
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
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
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
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.
|
||||
'''
|
||||
|
||||
import os
|
||||
|
||||
|
||||
def makeZip(sourceDir, outFilename, compression=8, exceptFile=None):
|
||||
@ -18,6 +27,8 @@ def makeZip(sourceDir, outFilename, compression=8, exceptFile=None):
|
||||
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):
|
||||
@ -30,10 +41,12 @@ def makeZip(sourceDir, outFilename, compression=8, exceptFile=None):
|
||||
zipf.close()
|
||||
|
||||
|
||||
|
||||
class midiConvert:
|
||||
def __init__(self, midiFile: str, outputPath: str):
|
||||
'''简单的midi转换类,将midi文件转换为我的世界结构或者包'''
|
||||
|
||||
import mido
|
||||
|
||||
self.midiFile = midiFile
|
||||
'''midi文件路径'''
|
||||
self.midi = mido.MidiFile(self.midiFile)
|
||||
@ -44,10 +57,7 @@ class midiConvert:
|
||||
self.midFileName = os.path.splitext(os.path.basename(self.midiFile))[0]
|
||||
'''文件名,不含路径且不含后缀'''
|
||||
|
||||
|
||||
|
||||
|
||||
def __Inst2SoundID(self,instrumentID, default='note.harp'):
|
||||
def __Inst2SoundID(self, instrumentID, default='note.harp'):
|
||||
'''返回midi的乐器ID对应的我的世界乐器名
|
||||
:param instrumentID: midi的乐器ID
|
||||
:param default: 如果instrumentID不在范围内,返回的默认我的世界乐器名称
|
||||
@ -84,8 +94,9 @@ class midiConvert:
|
||||
return 'note.xylophone'
|
||||
return default
|
||||
|
||||
|
||||
def _toCmdList_m1(self,scoreboardname : str = 'mscplay',volume:float = 1.0, speed:float = 1.0) -> list:
|
||||
def _toCmdList_m1(
|
||||
self, scoreboardname: str = 'mscplay', volume: float = 1.0, speed: float = 1.0
|
||||
) -> list:
|
||||
'''使用Dislink Sforza的转换算法,将midi转换为我的世界命令列表
|
||||
:param scoreboardname: 我的世界的计分板名称
|
||||
:param volume: 音量,注意:这里的音量范围为(0,1],如果超出将被处理为正确值,其原理为在距离玩家 (1 / volume -1) 的地方播放音频
|
||||
@ -96,31 +107,53 @@ class midiConvert:
|
||||
volume = 1
|
||||
if volume <= 0:
|
||||
volume = 0.001
|
||||
|
||||
|
||||
commands = 0
|
||||
|
||||
for i, track in enumerate(self.midi.tracks):
|
||||
|
||||
ticks=0
|
||||
commands=0
|
||||
instrumentID=0
|
||||
ticks = 0
|
||||
instrumentID = 0
|
||||
singleTrack = []
|
||||
|
||||
for msg in track:
|
||||
if msg.is_meta:
|
||||
if msg.type == 'set_tempo':
|
||||
tempo=msg.tempo
|
||||
tempo = msg.tempo
|
||||
if msg.type == 'program_change':
|
||||
instrumentID=msg.program
|
||||
instrumentID = msg.program
|
||||
else:
|
||||
ticks+=msg.time
|
||||
ticks += msg.time
|
||||
if msg.type == 'note_on' and msg.velocity != 0:
|
||||
singleTrack.append('execute @a[scores={'+scoreboardname+'='+str(round((ticks*tempo)/((self.midi.ticks_per_beat*float(speed))*50000)))+'}'+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)}')
|
||||
commands+=1
|
||||
singleTrack.append(
|
||||
'execute @a[scores={'
|
||||
+ scoreboardname
|
||||
+ '='
|
||||
+ str(
|
||||
round(
|
||||
(ticks * tempo)
|
||||
/ (
|
||||
(self.midi.ticks_per_beat * float(speed))
|
||||
* 50000
|
||||
)
|
||||
)
|
||||
)
|
||||
+ '}'
|
||||
+ 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)}'
|
||||
)
|
||||
commands += 1
|
||||
|
||||
tracks.append(singleTrack)
|
||||
|
||||
return tracks
|
||||
|
||||
def tomcpack(self,method:int = 1,scoreboardname : str = 'mscplay',volume:float = 1.0, speed:float = 1.0) -> bool:
|
||||
|
||||
return tracks, commands
|
||||
|
||||
def tomcpack(
|
||||
self,
|
||||
method: int = 1,
|
||||
scoreboardname: str = 'mscplay',
|
||||
volume: float = 1.0,
|
||||
speed: float = 1.0,
|
||||
) -> bool:
|
||||
'''使用method指定的转换算法,将midi转换为我的世界mcpack格式的包
|
||||
:param method: 转换算法
|
||||
:param scoreboardname: 我的世界的计分板名称
|
||||
@ -128,10 +161,14 @@ class midiConvert:
|
||||
:param speed: 速度,注意:这里的速度指的是播放倍率,其原理为在播放音频的时候,每个音符的播放时间除以 speed
|
||||
:return 成功与否,成功返回(True,True),失败返回(False,str失败原因)'''
|
||||
if method == 1:
|
||||
cmdlist = self._toCmdList_m1(scoreboardname,volume,speed)
|
||||
cmdlist, _a = self._toCmdList_m1(scoreboardname, volume, speed)
|
||||
else:
|
||||
return (False,f'无法找到算法ID{method}对应的转换算法')
|
||||
|
||||
return (False, f'无法找到算法ID{method}对应的转换算法')
|
||||
del _a
|
||||
|
||||
import json
|
||||
import uuid
|
||||
import shutil
|
||||
|
||||
# 当文件f夹{self.outputPath}/temp/functions存在时清空其下所有项目,若其不存在则创建
|
||||
if os.path.exists(f'{self.outputPath}/temp/functions/'):
|
||||
@ -141,29 +178,224 @@ class midiConvert:
|
||||
# 写入manifest.json
|
||||
if not os.path.exists(f'{self.outputPath}/temp/manifest.json'):
|
||||
with open(f"{self.outputPath}/temp/manifest.json", "w") as f:
|
||||
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}")
|
||||
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}"
|
||||
)
|
||||
else:
|
||||
with open(f'{self.outputPath}/temp/manifest.json', 'r') as manifest:
|
||||
data=json.loads(manifest.read())
|
||||
data['header']['description']=f"the Player of the Music {self.midFileName}"
|
||||
data['header']['name']=self.midFileName
|
||||
data['header']['uuid']=str(uuid.uuid4())
|
||||
data['modules'][0]['description']='None'
|
||||
data['modules'][0]['uuid']=str(uuid.uuid4())
|
||||
manifest.close()
|
||||
open(f'{self.outputPath}/temp/manifest.json','w').write(json.dumps(data))
|
||||
data = json.loads(manifest.read())
|
||||
data['header'][
|
||||
'description'
|
||||
] = f"the Player of the Music {self.midFileName}"
|
||||
data['header']['name'] = self.midFileName
|
||||
data['header']['uuid'] = str(uuid.uuid4())
|
||||
data['modules'][0]['description'] = 'None'
|
||||
data['modules'][0]['uuid'] = str(uuid.uuid4())
|
||||
manifest.close()
|
||||
open(f'{self.outputPath}/temp/manifest.json', 'w').write(json.dumps(data))
|
||||
|
||||
# 将命令列表写入文件
|
||||
indexfile = open(f'{self.outputPath}/temp/functions/index.mcfunction', 'w', encoding='utf-8')
|
||||
indexfile = open(
|
||||
f'{self.outputPath}/temp/functions/index.mcfunction', 'w', encoding='utf-8'
|
||||
)
|
||||
for track in cmdlist:
|
||||
indexfile.write('function mscplay/track'+str(cmdlist.index(track)+1)+'\n')
|
||||
with open(f'{self.outputPath}/temp/functions/mscplay/track{cmdlist.index(track)+1}.mcfunction','w',encoding='utf-8') as f:
|
||||
indexfile.write(
|
||||
'function mscplay/track' + str(cmdlist.index(track) + 1) + '\n'
|
||||
)
|
||||
with open(
|
||||
f'{self.outputPath}/temp/functions/mscplay/track{cmdlist.index(track)+1}.mcfunction',
|
||||
'w',
|
||||
encoding='utf-8',
|
||||
) as f:
|
||||
f.write('\n'.join(track))
|
||||
indexfile.write('scoreboard players add @a[scores={'+scoreboardname+'=1..}] '+scoreboardname+' 1\n')
|
||||
indexfile.write(
|
||||
'scoreboard players add @a[scores={'
|
||||
+ scoreboardname
|
||||
+ '=1..}] '
|
||||
+ scoreboardname
|
||||
+ ' 1\n'
|
||||
)
|
||||
indexfile.close()
|
||||
|
||||
makeZip(f'{self.outputPath}/temp/',self.outputPath+f'/{self.midFileName}.mcpack')
|
||||
makeZip(
|
||||
f'{self.outputPath}/temp/', self.outputPath + f'/{self.midFileName}.mcpack'
|
||||
)
|
||||
|
||||
shutil.rmtree(f'{self.outputPath}/temp/')
|
||||
|
||||
|
||||
def toBDXfile(
|
||||
self,
|
||||
method: int,
|
||||
author: str,
|
||||
maxheight: int,
|
||||
scoreboardname: str = 'mscplay',
|
||||
volume: float = 1.0,
|
||||
speed: float = 1.0,
|
||||
) -> bool:
|
||||
'''使用method指定的转换算法,将midi转换为BDX结构文件
|
||||
:param method: 转换算法
|
||||
:param scoreboardname: 我的世界的计分板名称
|
||||
:param volume: 音量,注意:这里的音量范围为(0,1],如果超出将被处理为正确值,其原理为在距离玩家 (1 / volume -1) 的地方播放音频
|
||||
:param speed: 速度,注意:这里的速度指的是播放倍率,其原理为在播放音频的时候,每个音符的播放时间除以 speed
|
||||
:return 成功与否,成功返回(True,未经过压缩的源),失败返回(False,str失败原因)'''
|
||||
|
||||
import brotli
|
||||
|
||||
if method == 1:
|
||||
cmdlist, totalcount = self._toCmdList_m1(scoreboardname, volume, speed)
|
||||
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'
|
||||
|
||||
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`
|
||||
悬浮字
|
||||
:param 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(self, total: int, maxHeight: int):
|
||||
'''给定总方块数量和最大高度,返回所构成的图形外切正方形的边长
|
||||
:param total: 总方块数量
|
||||
:param maxHeight: 最大高度
|
||||
:return: 外切正方形的边长 int'''
|
||||
import math
|
||||
|
||||
math.ceil(math.sqrt(total / maxHeight))
|
||||
|
||||
_sideLength = __fillSquareSideLength(totalcount, maxheight)
|
||||
|
||||
yforward = True
|
||||
zforward = True
|
||||
|
||||
nowy = 0
|
||||
nowz = 0
|
||||
|
||||
for track in cmdlist:
|
||||
for cmd in track:
|
||||
_bytes += __formCMDblk(
|
||||
cmd,
|
||||
(1 if yforward else 0)
|
||||
if (nowy != 0) and (nowy != maxheight)
|
||||
else (3 if zforward else 2),
|
||||
impluse=2,
|
||||
condition=False,
|
||||
needRedstone=False,
|
||||
tickDelay=0,
|
||||
customName='',
|
||||
executeOnFirstTick=False,
|
||||
trackOutput=True,
|
||||
)
|
||||
nowy += 1 if yforward else -1
|
||||
_bytes += key[y][int(yforward)]
|
||||
if ((nowy > maxheight) and (yforward)) or (
|
||||
(nowy < 0) and (not yforward)
|
||||
):
|
||||
yforward = not yforward
|
||||
nowz += 1 if zforward else -1
|
||||
_bytes += key[z][int(zforward)]
|
||||
if ((nowz > _sideLength) and (zforward)) or (
|
||||
(nowz < 0) and (not zforward)
|
||||
):
|
||||
zforward = not zforward
|
||||
_bytes += key[x][1]
|
||||
|
||||
with open(f"{self.outputPath}/{self.midFileName}.bdx", "ab+") as f:
|
||||
f.write(brotli.compress(_bytes + b'XE'))
|
||||
|
||||
return (True, _bytes)
|
||||
|
Loading…
Reference in New Issue
Block a user