diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..822e5a2 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +*.yaml linguist-language=Python +*.xml linguist-language=Python +*.md linguist-language=Python \ No newline at end of file diff --git a/.gitignore b/.gitignore index 424d384..c6f0fec 100644 --- a/.gitignore +++ b/.gitignore @@ -1,18 +1,21 @@ +# sth. can't open +/msctPkgver/secrets/*.py +/msctPkgver/secrets/*.c + + # mystuff -.vscode +/.vscode *.mid *.midi *.mcpack *.bdx +*.json -# Byte-compiled / optimized / DLL files +# Byte-compiled / optimized __pycache__/ -*.py[cod] +*.pyc *$py.class -# C extensions -*.so - # Distribution / packaging .Python build/ @@ -143,3 +146,12 @@ dmypy.json # Cython debug symbols cython_debug/ + +# Pycharm +/.idea + +# log +/.log + +# package +.7z diff --git a/README.md b/README.md index 4faccf4..7b4234a 100644 --- a/README.md +++ b/README.md @@ -22,118 +22,48 @@ 简体中文 | [English](README_EN.md) -## 软件介绍🚀 +## 介绍🚀 -音·创 Musicreater 是一款免费开源的 **《我的世界:基岩版》** 音乐制作软件 +音·创 是一个免费开源的针对 **《我的世界》** 的midi音乐转换库 欢迎加群:[861684859](https://jq.qq.com/?_wv=1027&k=hpeRxrYr) -**注意注意注意!!!当前版本正在进行代码重构,详细信息请进入QQ群了解!!** +## 文档📄 -## 软件作者✒ +[生成文件的使用](./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) -金羿 Eilles:我的世界基岩版指令师,个人开发者,B站不知名UP主,南昌在校高中生。 +[仓库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主,江西在校高中生。 诸葛亮与八卦阵 bgArray:我的世界基岩版玩家,喜欢编程和音乐,深圳初二学生。 -## 软件架构🏢 +## 致谢🙏 +本致谢列表排名无顺序。 -软件采用 *Python* 作为第一语言,目前还没有使用其他语言辅助。 - -支持 Windows7+ 以及各个支持 Python3.6+ 的 Linux - -***各位开发人员注意!!!多语言支持请使用函数`_`加载文字!!!如需补充,请在简体中文的语言文件(zh-CN.lang)中补充!!!*** - -## 使用教程📕 - -***请注意!音·创是音乐的 编辑 软件,不是转换软件,若要直接转换midi音乐到我的世界,请见 [音·创 包版本](https://gitee.com/EillesWan/Musicreater/blob/pkgver/)*** - -### 安装教程 - -下载[音·创自动安装器](https://gitee.com/EillesWan/Musicreater/releases/),将其放在你希望安装音·创的位置,运行后将自动安装。 - -提示:下载源最好选择\"2 GitHub\"。 - -### 从源代码运行教程 - -#### Windows7+ - -0. [Gitee下载(需要登陆)](https://gitee.com/EillesWan/Musicreater) - [Github下载(慢)](https://github.com/EillesWan/Musicreater)本程序源代码 -1. 安装Python 3.8.10 - [下载64位Python安装包](https://www.python.org/ftp/python/3.8.10/python-3.8.10-amd64.exe) - [下载32位Python安装包](https://www.python.org/ftp/python/3.8.10/python-3.8.10.exe) -2. 以管理员身份运行 作者的自我修养.py : - - 点击 “开始” 菜单,搜索 `命令提示符` - - 右键点击 `命令提示符` 左键点击 “以管理员身份运行” - - 将 “作者的自我修养.py” 拖拽入开启的窗口,按下回车 -3. 等待安装完成后,双击运行 Musicreater.py - -#### Linux - -0. 若你没有足够优秀的环境,推荐先在终端敲: -```bash -sudo apt-get update -sudo apt-get upgrade -sudo apt-get install python3 -sudo apt-get install python3-pip -sudo apt-get install git -``` -1. 若你足够自信,该整的都整了,就在你想下载此程序的地方打开终端,敲: -```bash -sudo git clone https://gitee.com/EillesWan/Musicreater.git -cd Musicreater -python3 作者的自我修养.py -python3 Musicreater.py -``` - -### 使用说明 - -1. 直接运行就好 -2. 后期会出详细的使用教程 -3. 如果在使用过程中发现了bug拜托请上报给我,详见下方联系方式 - -## 诸葛亮与八卦阵的关于羽音缭绕资源包应用地说明(不必要)📖 - -0. ***注意注意!!!这一条说明是供给老版本(Delta 0.1.x)使用的*** -1. 首先!这里的提示是给想使用多音色资源包的人的,如果你想用就请下载 [神羽资源包(神羽自己的链接)](https://pan.baidu.com/s/11uoq5zwN7c3rX-98DqVpJg)提取码:ek3t -2. 下载到你自己电脑上某个位置,可以不放置于本项目下。音色资源包较大,可以选取只下载: - `神羽资源包_乐器、音源的资源包\羽音缭绕-midiout_25.0` 这个文件夹,再嫌麻烦的话,也可以只下载其中的: - `神羽资源包_乐器\音源的资源包\羽音缭绕-midiout_25.0\mcpack(国际版推荐)格式_25.0` 或者: - `神羽资源包_乐器\音源的资源包\羽音缭绕-midiout_25.0\zip格式_25.0` -4. 接下来就是关键了:在*音创*中绑定资源包 - 首先,先打开 *音创*->帮助与疑问->\[神羽资源包位置选择\]:选择文件夹... 这时候,会跳出选择框 - 关键来了,选择:***您下载的`羽音缭绕-midiout_25.0`文件夹,或者`mcpack(国际版推荐)格式_25.0`或`zip格式_25.0`的上级目录*** - 举个例子:我的文件路径是这样的: - `L:\shenyu\音源的资源包\羽音缭绕-midiout_25.0`这里面有:`神羽资源包_25.0_使用方法.xls`、 - `mcpack(国际版推荐)格式_25.0`、`zip格式_25.0`两个文件夹和一个.xls文件,而你在音创中 - 也应该选择这个文件夹:**L:\shenyu\音源的资源包\羽音缭绕-midiout_25.0** -6. 如果你想使用音色资源包来制作函数,那么解析时你应该用 *音创*->编辑->从midi导入音轨且用新方法解析, - 然后再使用 *音创*->函数(包)->下面的四个新函数 - -## 致谢列表🙏 - -- 感谢由 **[Fuckcraft](https://github.com/fuckcraft)** **鸣凤鸽子**等 带来的我的世界websocket服务器功能 - 感谢 **昀梦**\ 找出指令生成错误bug并指正 -- 感谢由 **Charlie_Ping** “**查理平**” 带来的bdx转换功能 -- 感谢由 **CMA_2401PT** 带来的 BDXWorkShop 供本程序对于bdx操作的指导 -- 感谢由 **神羽** (**Miracle Plume**)\带来的羽音缭绕基岩版音色资源包 -- 感谢 **Arthur Morgan**\ 对本程序的排错提出了最大的支持 -- 感谢由 **Dislink Sforza** \带来的midi音色解析以及转换指令的算法,我们将其改编并应用 -- 感谢 **Touch** “**偷吃**” \提供的测试支持,并对程序的改进提供了丰富的意见 -- 感谢 **Mono**\反馈安装时的问题 +- 感谢由 **Charlie_Ping “查理平”** 带来的BDX文件转换参考,以及MIDI-我的世界对应乐器参考表格 +- 感谢由 **[CMA_2401PT](https://github.com/CMA2401PT)** 为我们的软件开发的一些方面进行指导,同时我们参考了他的BDXworkshop作为BDX结构编辑的参考 +- 感谢由 **[Dislink Sforza](https://github.com/Dislink) “断联·斯福尔扎”**\ 带来的midi音色解析以及转换指令的算法,我们将其改编并应用;同时,感谢他的[网页版转换器](https://dislink.github.io/midi2bdx/)给我们的开发与更新带来巨大的压力和动力,让我们在原本一骑绝尘的摸鱼道路上转向开发,希望他能考上一个理想的大学! +- 感谢 **Touch “偷吃”**\ 提供的BDX导入测试支持,并对程序的改进提供了丰富的意见;同时也感谢他的不断尝试新的内容,使我们的排错更进一步 +- 感谢 **Mono**\ 反馈安装时的问题,辅助我们找到了视窗操作系统下的兼容性问题 +- 感谢 **Ammelia “艾米利亚”**\ 敦促我们进行新的功能开发,并为新功能提出了非常优秀的大量建议,以及提供的BDX导入测试支持,为我们的新结构生成算法提供了大量的实际理论支持 +- 感谢 **[神羽](https://gitee.com/snowykami) “[SnowyKami](https://github.com/snowyfirefly)”** 对我们项目的支持与宣传 +- 感谢 **指令师_苦力怕 playjuice123**\为我们的程序找出错误,并提醒我们修复一个一直存在的大bug。 +- 感谢 **雷霆**\为我们的程序找出错误,并提醒修复bug。 -> 感谢广大群友为此程序提供的测试等支持 +> 感谢广大群友为此程序提供的测试等支持 > -> 若您对我们有所贡献但您的名字没有显示在此列表中,请联系我! +> 若您对我们有所贡献但您的名字没有显示在此列表中,请联系我们! -## 联系我们📞 +## 联系📞 +若遇到库中的问题,欢迎在[此](https://gitee.com/TriM-Organization/Musicreater/issues/new)提出你的issue。 +如果需要与开发组进行交流,欢迎加入我们的[开发闲聊Q群](https://jq.qq.com/?_wv=1027&k=hpeRxrYr)。 -## 待办事项⏲ - -- [] 喵喵喵 [Bilibili: 凌云金羿]: https://img.shields.io/badge/Bilibili-%E5%87%8C%E4%BA%91%E9%87%91%E7%BE%BF-00A1E7?style=for-the-badge diff --git a/README_EN.md b/README_EN.md index b596198..eca20b6 100644 --- a/README_EN.md +++ b/README_EN.md @@ -17,121 +17,52 @@ [![][license]](LICENSE) [![][release]](../../releases) -[简体中文](README.md) | English +[简体中文🇨🇳](README.md) | English🇬🇧 - - -**Notice that the language support of *README* may be a little SLOW.** +**Notice that the language translation of *Musicreater* may be a little SLOW.** ## Introduction🚀 -Musicreater(音·创) is an free open source software which is used for making and also creating music in **Minecraft: Bedrock Edition**. +Musicreater is a free open-source library used for converting midi file into formats that could be read in *Minecraft*. Welcome to join our QQ group: [861684859](https://jq.qq.com/?_wv=1027&k=hpeRxrYr) -**ATTENTION!** This software is under testing and developing, there is still a lot of bugs needed to be fixed. Please use it wisely. +## 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) ### Authors✒ -Eilles (金羿):A high school student, individual developer, unfamous BilibiliUPer, which knows a little about commands in *Minecraft: Bedrock Edition* +Eilles (金羿):A senior high school student, individual developer, unfamous Bilibili UPer, which knows a little about commands in *Minecraft: Bedrock Edition* -bgArray "诸葛亮与八卦阵": Fix bugs, improve code aesthetics, add new functions, change data format, etc. +bgArray "诸葛亮与八卦阵": A junior high school student, player of *Minecraft: Bedrock Edition*, which is a fan of music and programming. -### Framework🏢 - -Developed under *Python3.8 3.9*. However, theoretically support Python3.6+. - -Support Windows7+ && Linux (that supports Python3.6+) - -***ATTENTION TO DEVELOPERS!!! TO SUPPORT DIFFERENT LANGUAGES, PLEASE USE FUNCTION(METHOD) `_` TO LOAD TEXTs!!! IF YOU NEED TO SUPPLEMENT, PLEASE ADD THEM IN SIMPLEFIED CHINESE\'S LANGUAGE FILE(zh-CN.lang), WHEATHER WHAT LANGUAGE YOU USE!!!*** - -## Instructions📕 - -### Installation - -Download the *[MSCT Auto Installer](https://github.com/EillesWan/Musicreater/releases/tag/v0.2.0.0-Delta)*, put it in a directory that you want to install *Musicreater* into. Then run the auto installer and it will help you to install the *Musicreator* as well as Python3.8(if you haven\'t install it) - -Tips: You'd better choose the \"2 GitHub\" download source - -### Run with Source Code - -#### Windows7+ - -0. First, download the source code pack of Musicreater. - [Download from Gitee (Need to Login)](https://gitee.com/EillesWan/Musicreater/repository/archive/master.zip) - [Download from Github](https://github.com/EillesWan/Musicreater/archive/refs/heads/master.zip) -1. Install Python 3.8.10 - [Download the 64-bit Python Installer](https://www.python.org/ftp/python/3.8.10/python-3.8.10-amd64.exe) - [Download the 32-bit Python Installer](https://www.python.org/ftp/python/3.8.10/python-3.8.10.exe) -2. After completing installation, we need to install the libraries : - - Open "Start Menu" and find `cmd` - - Run `cmd` as Administrator - - Drag "作者的自我修养.py" into the opened window and press Enter -3. After completing installation,double click Musicreater.py to run - -#### Linux - -0. If you 're not sure whether your environment is good enough, please run these commands on Terminal -```bash -sudo apt-get update -sudo apt-get upgrade -sudo apt-get install python3 -sudo apt-get install python3-pip -sudo apt-get install git -``` -1. Now if you are confident enough about your runtime environment, open Terminal on the place which you want to download Musicreater, and run these -```bash -sudo git clone https://gitee.com/EillesWan/Musicreater.git -cd Musicreater -python3 作者的自我修养.py -python3 Musicreater.py -``` - -### Instructions of Using - -1. Just run Musicreater.pyc(or .py) if you have installed well -2. Detailed instructions is coming soon -3. If you find a bug, could you please report it to me? My contact info is right below. - -## Explanation of the use of *PlumeAudioSurrounding Resource Pack* by bgArray (unnecessary)📖 - -1. First! The tips here are for those who want to use the multi tone resource package, [Shenyu resource package (Shenyu's own link)](https://pan.baidu.com/s/11uoq5zwN7c3rX-98DqVpJg) \(Extraction code: `ek3t`\) -2. Download it to any location on your PC. Note that it does ***not*** need to be placed in the directory where *Musicreater* are. The audio resource package is large, so you can choose to download only:`神羽资源包_乐器、音源的资源包\羽音缭绕-midiout_25.0`. - Also, you can download only `神羽资源包_乐器\音源的资源包\羽音缭绕-midiout_25.0\mcpack(国际版推荐)格式_25.0` or - `神羽资源包_乐器\音源的资源包\羽音缭绕-midiout_25.0\zip格式_25.0`. -4. The next step is the most IMPORTANT: to bind the resource package to *Musicreater* - First, open *Musicreater*->Q&A->Select \[MiraclePlumeResourcePack\]... .At this time, in the selection box, - the IMPORTANT step comes, select: ***The directory you downloaded: `羽音缭绕-midiout_25.0`, or also the parent directory `mcpack(国际版推荐)格式_25.0`or`zip格式_25.0`*** - For example, my file path is as follows: - `L:\shenyu\音源的资源包\羽音缭绕-midiout_25.0` and in the directory, there are two folders and one .xls file: - `神羽资源包_25.0_使用方法.xls`, `mcpack(国际版推荐)格式_25.0` and `zip格式_25.0`, so in *Musicreater* you should also select this folder: **L:\shenyu\音源的资源包\羽音缭绕-midiout_25.0** -6. If you want to use the Miracle Plume Bedrock Edition Audio Resource Pack to make .mcfunction s, you should use Musicreater -> Edit - > Import audio tracks from MIDI and parse them with a new method, and then use it -Musicreater - > function (package) - > the following four new functions ## Thanks🙏 +This list is not in any order. -1. Thank [Fuckcraft](https://github.com/fuckcraft) *(“鸣凤鸽子” ,etc)* for the function of Creating the Websocket Server for Minecraft: Bedrock Edition. - - *!! They have given me the rights to directly copy the lib into Musicreater* -2. Thank *昀梦*\ for finding and correcting the bugs in the commands that *Musicreater* Created. -3. Thank *Charlie_Ping “查理平”* for bdx convert funtion. -4. Thank *CMA_2401PT* for BDXWorkShop as the .bdx structure's operation guide. -5. Thank *Miracle Plume “神羽”* \ for the Miracle Plume Bedrock Edition Audio Resource Pack -6. Thank *Arthur Morgan* for his/her biggest support for the debugging of Musicreater -7. Thanks for a lot of groupmates who support me and help me to test the program. -8. If you have give me some help but u haven't been in the list, please contact me. +- Thank *昀梦*\ for finding and correcting the bugs in the commands that *Musicreater* generated. +- Thank *Charlie_Ping “查理平”* for bdx convert function for reference, and the chart that's used to convert the mid's instruments into Minecraft's instruments. +- Thank *[CMA_2401PT](https://github.com/CMA2401PT)* for BDXWorkShop for reference of the .bdx structure's operation, and his guidance in some aspects of our development. +- Thank *[Dislink Sforza](https://github.com/Dislink) “断联·斯福尔扎”* \ for his midi analysis algorithm brought to us, we had adapted it and made it applied in one of our working method; Also, thank him for the [WebConvertor](https://dislink.github.io/midi2bdx/) which brought us so much pressure and power to develop as well as update our projects better, instead of loaf on our project. We hope he can get into a good university as he wantted to! +- Thank *Touch “偷吃”*\ for support of debugging and testing program and algorithm, as well his/her suggestions to the improvement of our project +- Thank *Mono*\ for reporting problems while installing +- Thank *Ammelia “艾米利亚”*\ for urging us to develop new functions, and put forward a lot of excellent suggestions for new functions, as well as the BDX file's importing test support provided, which has given a lot of practical theoretical support for our new Structure Generating Algorithm +- 感谢 *[神羽](https://gitee.com/snowykami) “[SnowyKami](https://github.com/snowyfirefly)”* for supporting and promoting our project -## Contact Information📞 +> Thanks for a lot of groupmates's support and help +> +> If you have given contribution but haven't been in the list, please contact us! -### Author *Eilles*(金羿) +## Contact Us📞 -1. QQ 2647547478 -2. E-mail EillesWan2006@163.com W-YI_DoctorYI@outlook.com EillesWan@outlook.com -3. WeChat WYI_DoctorYI - -### Author *bgArray*(诸葛亮与八卦阵) - -1. QQ 4740437765 +Meet problems? Welcome to give out your issue [here](https://gitee.com/EillesWan/Musicreater/issues/new)! +Want to get in contact of developers? Welcome to join our [Chat QQ group](https://jq.qq.com/?_wv=1027&k=hpeRxrYr). diff --git a/docs/库的生成与功能文档.md b/docs/库的生成与功能文档.md new file mode 100644 index 0000000..91bdb93 --- /dev/null +++ b/docs/库的生成与功能文档.md @@ -0,0 +1,187 @@ +

音·创 Musicreater

+ +

库版 Package Version

+ +

+ +

+ +**此为开发相关文档,内容包括:所生成文件结构的详细说明、特殊参数的详细解释** + +# 库的简单调用 + +参见[magicDemo.py的相关部分](../magicDemo.py#L436),使用此库进行MIDI转换非常简单。 + +```python +import msctPkgver # 导入转换库 + +# 首先新建转换对象。 +conversion = msctPkgver.midiConvert() +# 值得注意的是,一个转换对象可以转换多个文件。 +# 也就是在实例化的时候不进行对文件的绑定。 +# 如果有调试需要,可以在实例化时传入参数 debug = True +# 如:conversion = msctPkgver.midiConvert(debug=True) + +# 设置输入输出地址,并指定execute指令语法 +# 地址都为字符串类型,不能传入文件流 +midi_path = "./where/you/place/.midi/files.mid" +output_folder = "./where/you/want2/convert/into/" +old_execute_format = False # 指定是否使用旧的execute指令语法(即1.18及以前的《我的世界:基岩版》语法) +conversion.convert(midi_path,output_folder,old_execute_format) + +# 进行转换并接受输出,具体的参数均在文档中有相关说明 +method_id = 2 # 指定使用的转换算法 +convertion_result = conversion.tomcpack(method_id,*prompts) + +# 转换结果是一个元组。 +# 若其转换成功,则前三位必为 +# True, 指令数量, 最大延迟 +# 其中,最大延迟可以理解为计分板的最大值 +# 如果转换失败,暂时还没有定返回值的规则 +# 但是有一点是肯定的,数据结构必定是元组 +print(convertion_result) +``` + + +# 生成文件结构 + +## 名词解释 + +|名词|解释|备注| +|--------|-----------|----------| +|指令区|一个用于放置指令系统的区域,通常是常加载区。|常见于服务器指令系统、好友联机房间中| +|指令链(链)|与链式指令方块不同,一个指令链通常指代的是一串由某种非链式指令方块作为开头,后面连着一串链式指令方块的结构。|通常的链都应用于需要“单次激活而多指令”的简单功能| +|起始块|链最初的那个非链式指令方块。|此方块为脉冲方块或重复方块皆可| +|指令系统(系统)|指令系统通常指的是,由一个或多个指令链以及相关红石机构相互配合、一同组成的,为达到某种特定的功能而构建的整体结构。|通常的系统都应用于需要“综合调配指令”的复杂功能。可由多个实现不同功能的模块构成,不同系统之间可以相互调用各自的模块。| +|游戏刻(刻)|游戏的一刻是指《我的世界》的游戏循环运行一次所占用的时间。([详见《我的世界》中文维基](https://minecraft.fandom.com/zh/wiki/%E5%88%BB#%E6%B8%B8%E6%88%8F%E5%88%BB))。指令方块的延迟功能(即指令方块的“延迟刻数”设置项,此项的名称被误译为“已选中项的延迟”)的单位即为`1`游戏刻。|正常情况下,游戏固定以每秒钟20刻的速率运行。但是,由于游戏内的绝大多数操作都是基于刻数而非现实中的时间来计时并进行的,一次游戏循环内也许会发生大量的操作,很多情况下,一秒对应的游戏刻会更少。 | + +## 文件格式 + +1. 附加包格式(`.mcpack`) + + 使用附加包格式导出音乐,则音乐会以指令函数文件(`.mcfunction`)存储于附加包内。在附加包中,函数文件的存储结构应为: + + - `functions\` + - `index.mcfunction` + - `mscply\` + - `progressShow.mcfunction` + - `track1.mcfunction` + - `track2.mcfunction` + - ... + - `trackN.mcfunction` + + 如图,其中,`index.mcfunction`文件和`mscply`文件夹存在于函数目录的根下;在`mscply`目录中,包含音乐导出的众多音轨播放文件(`trackX.mcfunction`),同时,若生成此包时选择了带有进度条的选项,则会包含`progressShow.mcfunction`文件。 + + `index.mcfunction`用于开始播放,其中包含打开各个音轨对应函数的指令,以及加分指令,这里的加分,是将**播放计分板的值大于等于`1`**的所有**玩家**的播放计分板分数增加`1`。同时,若生成此包时选择了自动重置计分板的选项,则会包含一条重置计分板的指令。 + + > 你知道吗?音·创的最早期版本“《我的世界》函数音乐生成器”正是用函数来播放,不过这个版本采取的读入数据的形式大有不同。 + +2. 结构格式 + + 无论是音·创生成的是何种结构,`MCSTRUCTURE`还是`BDX`,都会依照此处的格式来生成。此处我们想说明的结构的格式不是结构文件存储的格式,而是结构导出之后方块将如何摆放的问题。结构文件存储的格式这一点,在各个《我的世界》开发的相关网站上都可能会有说明。 + + 考虑到进行《我的世界》游戏开发时,为了节约常加载区域,很多游戏会将指令区设立为一种层叠式的结构。这种结构会限制每一层的指令系统的高度,但是虽然长宽也是有限的,却仍然比其纵轴延伸得更加自由。 + + 所以,结构的生成形状依照给定的高度和内含指令的数量决定。其$Z$轴延伸长度为指令方块数量对于给定高度之商的向下取整结果的平方根的向下取整。用数学公式的方式表达,则是: + + $$MaxZ = \left\lfloor\sqrt{\left\lfloor{\frac{NoC}{MaxH}}\right\rfloor}\right\rfloor$$ + + 其中,$MaxZ$即生成结构的$Z$轴最大延伸长度,$NoC$表示链结构中所含指令方块的个数,$MaxH$表示给定的生成结构的最大高度。 + + 我们的结构生成器在生成指令链时,将首先以相对坐标系$(0, 0, 0)$(即相对原点)开始,自下向上堆叠高度轴(即$Y$轴)的长,当高度轴达到了限制的高度时,便将$Z$轴向正方向堆叠`1`个方块,并开始自上向下重新堆叠,直至高度轴坐标达到相对为`0`。若当所生成结构的$Z$轴长达到了其最大延伸长度,则此结构生成器将反转$Z$轴的堆叠方向,直至$Z$轴坐标相对为`0`。如此往复,直至指令链堆叠完成。 + +## 播放器 + +以结构生成的文件可以采用多种方式播放,一类播放方式,我们称其为**播放器**,例如**延迟播放器**和**计分板播放器**等等,以后推出的新的播放器,届时也会在此处更新。 + +为什么要设计这么多播放器?是为了适应不同的播放环境需要。通常情况下,一个音乐中含有多个音符,音符与音符之间存在间隔,这里就产生了不一样的,实现音符间时间间隔的方式。而不同的应用环境下,又会产生不一样的要求。接下来将对不同的播放器进行详细介绍。 + +1. 计分板播放器 + + 计分板播放器是一种传统的《我的世界》音乐播放方式。通过对于计分板加分来实现播放不同的音符。一个很简单的原理,就是**用不同的计分板分值对应不同的音符**,再通过加分,来达到那个分值,即播放出来。 + + 在**音·创**中,用来达到这种效果的指令是这样的: + + ```mcfunction + execute @a[scores={ScBd=x}] ~ ~ ~ playsound InstID @s ~ ~Ht ~ Vlct Ptc + ``` + + |参数|说明|备注| + |--------|-----------|----------| + |`ScBd`|指定的计分板名称|| + |`x`|音发出时对应的分数值|| + |`InstID`|声音效果ID|不同的声音ID可以对应不同的乐器,详见转换[乐器对照表](./%E8%BD%AC%E6%8D%A2%E4%B9%90%E5%99%A8%E5%AF%B9%E7%85%A7%E8%A1%A8.md)| + |`Ht`|播放点对玩家的距离|通过距离来表达声音的响度。以$S$表示此参数`Ht`,以Vol表示音量百分比,则计算公式为:$S = \frac{1}{Vol}-1$| + |`Vlct`|原生我的世界中规定的播放力度|这个参数是一个谜一样的存在,似乎它的值毫不重要……因为无论这个值是多少,我们听起来都差不多。当此音符所在MIDI通道为第一通道,则这个值为0.7倍MIDI指定力度,其他则为0.9倍。| + |`Ptc`|音符的音高|这是决定音调的参数。以$P$表示此参数,$n$表示其在MIDI中的编号,$x$表示一定的音域偏移,则计算公式为:$P = 2^\frac{n-60-x}{12}$| + + 后四个参数决定了这个音的性质,而前两个参数仅仅是为了决定音播放的时间。 + +2. 延迟播放器 + + 延迟播放器是通过《我的世界》游戏中,指令方块的设置项“延迟刻数”来达到定位音符的效果。**将所有的音符依照其播放时距离乐曲开始时的时间(毫秒),放在一个序列内,再计算音符两两之间对应的时间差值,转换为《我的世界》内对应的游戏刻数之后填入指令方块的设置中。** + + 在音·创中,由于此方式播放的音乐不需要用计分板,所以播放指令是这样的: + + ```mcfunction + execute Tg ~ ~ ~ playsound InstID @s ~ ~Ht ~ Vlct Ptc + ``` + + |参数|说明|备注| + |--------|-----------|----------| + |`Tg`|播放对象|选择器或玩家名| + |`InstID`|声音效果ID|不同的声音ID可以对应不同的乐器,详见转换[乐器对照表](./%E8%BD%AC%E6%8D%A2%E4%B9%90%E5%99%A8%E5%AF%B9%E7%85%A7%E8%A1%A8.md)| + |`Ht`|播放点对玩家的距离|通过距离来表达声音的响度。以$S$表示此参数`Ht`,以Vol表示音量百分比,则计算公式为:$S = \frac{1}{Vol}-1$| + |`Vlct`|原生我的世界中规定的播放力度|这个参数是一个谜一样的存在,似乎它的值毫不重要……因为无论这个值是多少,我们听起来都差不多。当此音符所在MIDI通道为第一通道,则这个值为0.7倍MIDI指定力度,其他则为0.9倍。| + |`Ptc`|音符的音高|这是决定音调的参数。以$P$表示此参数,$n$表示其在MIDI中的编号,$x$表示一定的音域偏移,则计算公式为:$P = 2^\frac{n-60-x}{12}$| + + 其中后四个参数决定了这个音的性质。 + + 由于这样的延迟数据是依赖于指令方块的设置项,所以使用这种播放器所转换出的结果仅可以存储在包含方块NBT信息及方块实体NBT信息的结构文件中,或者直接输出至世界。 + + + +# 进度条自定义 + +因为我们提供了可以自动转换进度条的功能,因此在这里给出进度条自定义参数的详细解释。 + +请注意,并非所有的演示样例程序都支持自定义进度条。 + +一个进度条,明显地,有**固定部分**和**可变部分**来构成。而可变部分又包括了文字和图形两种(当然,《我的世界》里头的进度条,可变的图形也就是那个“条”了)。这一点你需要了解,因为后文中包含了很多这方面的概念需要你了解。 + +进度条的自定义功能使用一个字符串来定义自己的样式,其中包含众多**标识符**来表示可变部分。 + +标识符如下(注意大小写): + +| 标识符 | 指定的可变量 | +|---------|----------------| +| `%%N` | 乐曲名(即传入的文件名)| +| `%%s` | 当前计分板值 | +| `%^s` | 计分板最大值 | +| `%%t` | 当前播放时间 | +| `%^t` | 曲目总时长 | +| `%%%` | 当前进度比率 | +| `_` | 用以表示进度条占位| + +表示进度条占位的 `_` 是用来标识你的进度条的。也就是可变部分的唯一的图形部分。 + +**样式定义字符串**的样例如下,这也是默认的进度条的样式: + +`▶ %%N [ %%s/%^s %%% __________ %%t|%^t]` + +这是单独一行的进度条,当然你也可以制作多行的,如果是一行的,输出时所使用的指令便是 `title`,而如果是多行的话,输出就会用 `titleraw` 作为进度条字幕。 + +哦对了,上面的只不过是样式定义,同时还需要定义的是可变图形的部分,也就是进度条上那个真正的“条”。 + +对于这个我们就采用了固定参数的方法,对于一个进度条,无非就是“已经播放过的”和“没播放过的”两种形态,所以,使用一个元组来传入这两个参数就是最简单的了。元组的格式也很简单:`(str: 播放过的部分长啥样, str: 没播放过的部分长啥样)` 。例如,我们默认的进度“条”的定义是这样的: + +`('§e=§r', '§7=§r')` + +综合起来,把这些参数传给函数需要一个参数整合,你猜用的啥?啊对对对,我用的还是元组! + +我们的默认定义参数如下: + +`(r'▶ %%N [ %%s/%^s %%% __________ %%t|%^t]',('§e=§r', '§7=§r'))` + +*对了!为了避免生成错误,请尽量避免使用标识符作为定义样式字符串的其他部分* + diff --git a/docs/生成文件的使用说明.md b/docs/生成文件的使用说明.md new file mode 100644 index 0000000..fbd6950 --- /dev/null +++ b/docs/生成文件的使用说明.md @@ -0,0 +1,48 @@ +

音·创 Musicreater

+ +

库版 Package Version

+ +

+ +

+ +# 生成文件的使用 + +*由于先前的 **读我文件**(README.md) 过于冗杂,现另辟蹊径来给大家全方位的教程。* + +*这是本库所生成文件的使用声明,不是使用本库的教程,若要查看**本库的演示程序**使用教程,可点击[此处](%E5%8A%9F%E8%83%BD%E4%BD%BF%E7%94%A8%E8%AF%B4%E6%98%8E.md);若要查看有关文件结构的内容,可以点击[此处](./%E7%94%9F%E6%88%90%E6%96%87%E4%BB%B6%E7%BB%93%E6%9E%84%E8%AF%B4%E6%98%8E.md)* + +## 附加包格式 + +支持的文件后缀:`.MCPACK` + +1. 导入附加包 +2. 在一个循环方块中输入指令 `function index` +3. 将需要聆听音乐的实体的播放所用计分板设置为 `1` +4. 激活循环方块 +5. 若想要暂停播放,可以停止循环指令方块的激活状态 +6. 若想要重置某实体的播放,可以将其播放用的计分板重置 + +> 其中 步骤三 和 步骤四 的顺序可以调换。 + +## 结构格式 + +支持的文件后缀:`.MCSTRUCTURE`、`.BDX` + +1. 将结构导入世界 + +- 延迟播放器 + + 2. 将结构生成的第一个指令方块之模式更改为**脉冲** + 3. 激活脉冲方块 + 4. 若欲重置播放,可以停止对此链的激活,例如停止区块加载 + 5. 此播放器不支持暂停 + +- 计分板播放器 + + 2. 在所生成的第一个指令方块前,放置一个循环指令方块,其朝向应当对着所生成的第一个方块 + 3. 在循环指令方块中输入使播放对象的播放用计分板加分的指令,延迟为`0`,每次循环增加`1`分 + 4. 激活循环方块 + 5. 若想要暂停播放,可以停止循环指令方块的激活状态 + 6. 若想要重置某实体的播放,可以将其播放用的计分板重置 + diff --git a/docs/转换乐器对照表.md b/docs/转换乐器对照表.md new file mode 100644 index 0000000..8887e91 --- /dev/null +++ b/docs/转换乐器对照表.md @@ -0,0 +1,2 @@ + +# 暂无 diff --git a/msctPkgver/__init__.py b/msctPkgver/__init__.py new file mode 100644 index 0000000..62f373a --- /dev/null +++ b/msctPkgver/__init__.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +"""一个简单的我的世界音频转换库 +音·创 库版 (Musicreater) +是一款免费开源的针对《我的世界》的midi音乐转换库 +Musicreater(音·创) +A free open source library used for convert midi file into formats that is suitable for **Minecraft**. + +版权所有 © 2023 音·创 开发者 +Copyright © 2023 all the developers of Musicreater + +开源相关声明请见 ../License.md +Terms & Conditions: ../License.md +""" + +# 音·创 开发交流群 861684859 +# Email EillesWan2006@163.com W-YI_DoctorYI@outlook.com EillesWan@outlook.com +# 版权所有 金羿("Eilles Wan") & 诸葛亮与八卦阵("bgArray") & 鸣凤鸽子("MingFengPigeon") +# 若需转载或借鉴 许可声明请查看仓库目录下的 License.md + +from .main import * + +__version__ = "0.2.3" +__all__ = [] +__author__ = (("金羿", "Eilles Wan"), ("诸葛亮与八卦阵", "bgArray")) + + diff --git a/msctPkgver/exceptions.py b/msctPkgver/exceptions.py new file mode 100644 index 0000000..bc68d22 --- /dev/null +++ b/msctPkgver/exceptions.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- + + +# 音·创 开发交流群 861684859 +# Email EillesWan2006@163.com W-YI_DoctorYI@outlook.com EillesWan@outlook.com +# 版权所有 金羿("Eilles Wan") & 诸葛亮与八卦阵("bgArray") & 鸣凤鸽子("MingFengPigeon") +# 若需转载或借鉴 许可声明请查看仓库目录下的 License.md + + +""" +音·创 库版 (Musicreater Package Version) +是一款免费开源的针对《我的世界:基岩版》的midi音乐转换库 +Musicreater pkgver (Package Version 音·创 库版) +A free open source library used for convert midi file into formats that is suitable for **Minecraft: Bedrock Edition**. + +版权所有 © 2023 音·创 开发者 +Copyright © 2023 all the developers of Musicreater + +开源相关声明请见 ../License.md +Terms & Conditions: ../License.md +""" + + +class MSCTBaseException(Exception): + """音·创库版本的所有错误均继承于此""" + + def __init__(self, *args): + super().__init__(*args) + + def miao( + self, + ): + for i in self.args: + print(i + "喵!") + + def crash_it(self): + raise self + + +class CrossNoteError(MSCTBaseException): + """同通道下同音符交叉出现所产生的错误""" + + pass + + +class NotDefineTempoError(MSCTBaseException): + """没有Tempo设定导致时间无法计算的错误""" + + pass + + +class MidiDestroyedError(MSCTBaseException): + """Midi文件损坏""" + + pass + + +class ChannelOverFlowError(MSCTBaseException): + """一个midi中含有过多的通道(数量应≤16)""" + + pass + + +class NotDefineProgramError(MSCTBaseException): + """没有Program设定导致没有乐器可以选择的错误""" + + pass + + +class ZeroSpeedError(MSCTBaseException): + """以0作为播放速度的错误""" + + pass \ No newline at end of file diff --git a/msctPkgver/instConstants.py b/msctPkgver/instConstants.py new file mode 100644 index 0000000..d8a2615 --- /dev/null +++ b/msctPkgver/instConstants.py @@ -0,0 +1,179 @@ +pitched_instrument_list = { + 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.bd", 7), # 打击乐器无音域 + 117: ("note.bass", 8), + 118: ("note.bit", 6), + 119: ("note.bd", 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.bd", 7), # 打击乐器无音域 + 127: ("note.snare", 7), # 打击乐器无音域 +} + +percussion_instrument_list = { + 34: ("note.bd", 7), + 35: ("note.bd", 7), + 36: ("note.hat", 7), + 37: ("note.snare", 7), + 38: ("note.snare", 7), + 39: ("note.snare", 7), + 40: ("note.hat", 7), + 41: ("note.snare", 7), + 42: ("note.hat", 7), + 43: ("note.snare", 7), + 44: ("note.snare", 7), + 45: ("note.bell", 4), + 46: ("note.snare", 7), + 47: ("note.snare", 7), + 48: ("note.bell", 4), + 49: ("note.hat", 7), + 50: ("note.bell", 4), + 51: ("note.bell", 4), + 52: ("note.bell", 4), + 53: ("note.bell", 4), + 54: ("note.bell", 4), + 55: ("note.bell", 4), + 56: ("note.snare", 7), + 57: ("note.hat", 7), + 58: ("note.chime", 4), + 59: ("note.iron_xylophone", 6), + 60: ("note.bd", 7), + 61: ("note.bd", 7), + 62: ("note.xylophone", 4), + 63: ("note.xylophone", 4), + 64: ("note.xylophone", 4), + 65: ("note.hat", 7), + 66: ("note.bell", 4), + 67: ("note.bell", 4), + 68: ("note.hat", 7), + 69: ("note.hat", 7), + 70: ("note.flute", 5), + 71: ("note.flute", 5), + 72: ("note.hat", 7), + 73: ("note.hat", 7), + 74: ("note.xylophone", 4), + 75: ("note.hat", 7), + 76: ("note.hat", 7), + 77: ("note.xylophone", 4), + 78: ("note.xylophone", 4), + 79: ("note.bell", 4), + 80: ("note.bell", 4), } diff --git a/msctPkgver/magicmain.py b/msctPkgver/magicmain.py new file mode 100644 index 0000000..7b2a3cf --- /dev/null +++ b/msctPkgver/magicmain.py @@ -0,0 +1,217 @@ +# -*- coding: utf-8 -*- + + +# 音·创 开发交流群 861684859 +# 版权所有 金羿("Eilles Wan") & 诸葛亮与八卦阵("bgArray") & 鸣凤鸽子("MingFengPigeon") +# 若需使用或借鉴 请依照 Apache 2.0 许可证进行许可 + + +""" +音·创 库版 (Musicreater Package Version) +是一款免费开源的针对《我的世界:基岩版》的midi音乐转换库 +注意!除了此源文件以外,任何属于此仓库以及此项目的文件均依照Apache许可证进行许可 +Musicreater pkgver (Package Version 音·创 库版) +A free open source library used for convert midi file into formats that is suitable for **Minecraft: Bedrock Edition**. +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'); + 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. +""" + + +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) 的地方播放音频 + :param speed: 速度,注意:这里的速度指的是播放倍率,其原理为在播放音频的时候,每个音符的播放时间除以 speed + :return: tuple(命令列表, 命令个数, 计分板最大值) + """ + 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 = [] + + for msg in track: + ticks += msg.time + # print(msg) + if msg.is_meta: + if msg.type == "set_tempo": + tempo = msg.tempo + else: + if msg.type == "program_change": + # print("TT") + instrumentID = msg.program + if msg.type == "note_on" and msg.velocity != 0: + nowscore = round( + (ticks * tempo) + / ((self.midi.ticks_per_beat * float(speed)) * 50000) + ) + maxscore = max(maxscore, nowscore) + soundID, _X = self.__Inst2soundID_withX(instrumentID) + singleTrack.append( + "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)}") + commands += 1 + if len(singleTrack) != 0: + tracks.append(singleTrack) + + return [tracks, commands, maxscore] + + + + + + + + + +# ============================ + + + + +import mido + + + + + +class NoteMessage: + def __init__(self, channel, pitch, velocity, startT, lastT, midi, now_bpm, change_bpm=None): + self.channel = channel + self.note = pitch + self.velocity = velocity + self.startTime = startT + self.lastTime = lastT + self.tempo = now_bpm # 这里要程序实现获取bpm可以参考我的程序 + + def mt2gt(mt, tpb_a, bpm_a): + return mt / tpb_a / bpm_a * 60 + self.startTrueTime = mt2gt(self.startTime, midi.ticks_per_beat, self.tempo) # / 20 + # delete_extra_zero(round_up()) + if change_bpm is not None: + self.lastTrueTime = mt2gt(self.lastTime, midi.ticks_per_beat, change_bpm) # / 20 + else: + self.lastTrueTime = mt2gt(self.lastTime, midi.ticks_per_beat, self.tempo) # / 20 + # delete_extra_zero(round_up()) + print((self.startTime * self.tempo) / (midi.ticks_per_beat * 50000)) + + def __str__(self): + return "noteMessage channel=" + str(self.channel) + " note=" + str(self.note) + " velocity=" + \ + str(self.velocity) + " startTime=" + str(self.startTime) + " lastTime=" + str(self.lastTime) + \ + " startTrueTime=" + str(self.startTrueTime) + " lastTrueTime=" + str(self.lastTrueTime) + + +def load(mid: mido.MidiFile): + + type_ = [False, False, False] # note_off / note_on+0 / mixed + + is_tempo = False + + # 预检 + for i, track in enumerate(mid.tracks): + for msg in track: + # print(msg) + if msg.is_meta is not True: + if msg.type == 'note_on' and msg.velocity == 0: + type_[1] = True + elif msg.type == "note_off": + type_[0] = True + if msg.is_meta is True and msg.type == "set_tempo": + is_tempo = True + + if is_tempo is not True: + raise Exception("这个mid没有可供计算时间的tempo事件") + + if type_[0] is True and type_[1] is True: + type_[2] = True + type_[1] = False + type_[0] = False + print(type_) + + bpm = 0 + recent_change_bpm = 0 + is_change_bpm = False + # 实检 + for i, track in enumerate(mid.tracks): + noteOn = [] + trackS = [] + ticks = 0 + for msg in track: + print(msg) + ticks += msg.time + print(ticks) + if msg.is_meta is True and msg.type == "set_tempo": + recent_change_bpm = bpm + bpm = 60000000 / msg.tempo + is_change_bpm = True + + if msg.type == 'note_on' and msg.velocity != 0: + noteOn.append([msg, msg.note, ticks]) + if type_[1] is True: + if msg.type == 'note_on' and msg.velocity == 0: + for u in noteOn: + index = 0 + if u[1] == msg.note: + lastMessage = u[0] + lastTick = u[2] + break + index += 1 + print(lastTick) + if is_change_bpm and recent_change_bpm != 0: + trackS.append(NoteMessage(msg.channel, msg.note, lastMessage.velocity, lastTick, ticks - lastTick, + mid, recent_change_bpm, bpm)) + is_change_bpm = False + else: + trackS.append( + NoteMessage(msg.channel, msg.note, lastMessage.velocity, lastTick, ticks - lastTick, + mid, bpm)) + # print(noteOn) + # print(index) + try: + noteOn.pop(index) + except IndexError: + noteOn.pop(index - 1) + print(trackS) + for j in trackS: + print(j) + + +if __name__ == '__main__': + load(mido.MidiFile("test.mid")) + + + + + + diff --git a/msctPkgver/main.py b/msctPkgver/main.py new file mode 100644 index 0000000..09626a9 --- /dev/null +++ b/msctPkgver/main.py @@ -0,0 +1,1434 @@ +# -*- coding: utf-8 -*- + + +# 音·创 开发交流群 861684859 +# Email EillesWan2006@163.com W-YI_DoctorYI@outlook.com EillesWan@outlook.com +# 版权所有 金羿("Eilles Wan") & 诸葛亮与八卦阵("bgArray") & 鸣凤鸽子("MingFengPigeon") +# 若需转载或借鉴 许可声明请查看仓库目录下的 License.md + + +""" +音·创 (Musicreater) +是一款免费开源的针对《我的世界》的midi音乐转换库 +Musicreater (音·创) +A free open source library used for convert midi file into formats that is suitable for **Minecraft**. + +版权所有 © 2023 音·创 开发者 +Copyright © 2023 all the developers of Musicreater + +开源相关声明请见 ../License.md +Terms & Conditions: ../License.md +""" + +import mido +import brotli +import json +import uuid +import shutil + +from .utils import * +from .exceptions import * +from .instConstants import * + +from typing import TypeVar, Union + +T = TypeVar("T") # Declare type variable +VM = TypeVar("VM", mido.MidiFile, None) # void mido + + +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__() + + +""" +学习笔记: +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 => 毫秒数 + +########### + +seconds per tick: +(tempo / 1000000.0) / ticks_per_beat + +seconds: +tick * tempo / 1000000.0 / ticks_per_beat + +microseconds: +tick * tempo / 1000.0 / ticks_per_beat + +gameticks: +tick * tempo / 1000000.0 / ticks_per_beat * 一秒多少游戏刻 + + +""" + + +class midiConvert: + def __init__(self, debug: bool = False): + """简单的midi转换类,将midi文件转换为我的世界结构或者包""" + self.debugMode: bool = debug + + self.midiFile: str = "" + self.midi: VM = None + self.outputPath: str = "" + self.midFileName: str = "" + self.exeHead = "" + self.methods = MethodList( + [self._toCmdList_m1, self._toCmdList_m2, self._toCmdList_m3] + ) + + self.methods_byDelay = MethodList( + [ + self._toCmdList_withDelay_m1, + self._toCmdList_withDelay_m2, + ] + ) + + def convert(self, midiFile: str, outputPath: str, oldExeFormat: bool = True): + """转换前需要先运行此函数来获取基本信息""" + + self.midiFile = midiFile + """midi文件路径""" + + try: + self.midi = mido.MidiFile(self.midiFile) + """MidiFile对象""" + except Exception as E: + raise MidiDestroyedError(f"文件{self.midiFile}损坏:{E}") + + self.outputPath = os.path.abspath(outputPath) + """输出路径""" + # 将self.midiFile的文件名,不含路径且不含后缀存入self.midiFileName + self.midFileName = os.path.splitext(os.path.basename(self.midiFile))[0] + """文件名,不含路径且不含后缀""" + + self.exeHead = ( + "execute {} ~ ~ ~ " + if oldExeFormat + else "execute as {} at @s positioned ~ ~ ~ run " + ) + """execute指令的应用,两个版本提前决定。""" + + @staticmethod + def __Inst2soundID_withX(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 + 而存在一些打击乐器bd(basedrum)、hat、snare,没有音域,则没有X,那么我们返回7即可 + :param instrumentID: midi的乐器ID + default: 如果instrumentID不在范围内,返回的默认我的世界乐器名称 + :return: (str我的世界乐器名, int转换算法中的X)""" + try: + return pitched_instrument_list[instrumentID] + except KeyError: + return "note.flute", 5 + + @staticmethod + def __bitInst2ID_withX(instrumentID): + try: + return percussion_instrument_list[instrumentID] + except KeyError: + print("WARN", "无法使用打击乐器列表库,可能是不支持当前环境,打击乐器使用Dislink算法代替。") + if instrumentID == 55: + return "note.cow_bell", 5 + elif instrumentID in [41, 43, 45]: + return "note.hat", 7 + elif instrumentID in [36, 37, 39]: + return "note.snare", 7 + else: + return "note.bd", 7 + + @staticmethod + def score2time(score: int): + return str(int(int(score / 20) / 60)) + ":" + str(int(int(score / 20) % 60)) + + def __formProgressBar( + self, + maxscore: int, + scoreboard_name: str, + progressbar: tuple = ( + r"▶ %%N [ %%s/%^s %%% __________ %%t|%^t ]", + ("§e=§r", "§7=§r"), + ), + ) -> list: + + pgs_style = progressbar[0] + """用于被替换的进度条原始样式""" + + """ + | 标识符 | 指定的可变量 | + |---------|----------------| + | `%%N` | 乐曲名(即传入的文件名)| + | `%%s` | 当前计分板值 | + | `%^s` | 计分板最大值 | + | `%%t` | 当前播放时间 | + | `%^t` | 曲目总时长 | + | `%%%` | 当前进度比率 | + | `_` | 用以表示进度条占位| + """ + perEach = maxscore / pgs_style.count("_") + + result = [] + + if r"%^s" in pgs_style: + pgs_style = pgs_style.replace(r"%^s", str(maxscore)) + + if r"%^t" in pgs_style: + pgs_style = pgs_style.replace(r"%^t", self.score2time(maxscore)) + + sbn_pc = scoreboard_name[:2] + if r"%%%" in pgs_style: + result.append( + 'scoreboard objectives add {}PercT dummy "百分比计算"'.format(sbn_pc) + ) + result.append( + self.exeHead.format("@a[scores={" + scoreboard_name + "=1..}]") + + "scoreboard players set MaxScore {} {}".format( + scoreboard_name, maxscore + ) + ) + result.append( + self.exeHead.format("@a[scores={" + scoreboard_name + "=1..}]") + + "scoreboard players set n100 {} 100".format(scoreboard_name) + ) + result.append( + self.exeHead.format("@a[scores={" + scoreboard_name + "=1..}]") + + "scoreboard players operation @s {} = @s {}".format( + sbn_pc + "PercT", scoreboard_name + ) + ) + result.append( + self.exeHead.format("@a[scores={" + scoreboard_name + "=1..}]") + + "scoreboard players operation @s {} *= n100 {}".format( + sbn_pc + "PercT", scoreboard_name + ) + ) + result.append( + self.exeHead.format("@a[scores={" + scoreboard_name + "=1..}]") + + "scoreboard players operation @s {} /= MaxScore {}".format( + sbn_pc + "PercT", scoreboard_name + ) + ) + + if r"%%t" in pgs_style: + result.append( + 'scoreboard objectives add {}TMinT dummy "时间计算:分"'.format(sbn_pc) + ) + result.append( + 'scoreboard objectives add {}TSecT dummy "时间计算:秒"'.format(sbn_pc) + ) + result.append( + self.exeHead.format("@a[scores={" + scoreboard_name + "=1..}]") + + "scoreboard players set n20 {} 20".format(scoreboard_name) + ) + result.append( + self.exeHead.format("@a[scores={" + scoreboard_name + "=1..}]") + + "scoreboard players set n60 {} 60".format(scoreboard_name) + ) + + result.append( + self.exeHead.format("@a[scores={" + scoreboard_name + "=1..}]") + + "scoreboard players operation @s {} = @s {}".format( + sbn_pc + "TMinT", scoreboard_name + ) + ) + result.append( + self.exeHead.format("@a[scores={" + scoreboard_name + "=1..}]") + + "scoreboard players operation @s {} /= n20 {}".format( + sbn_pc + "TMinT", scoreboard_name + ) + ) + result.append( + self.exeHead.format("@a[scores={" + scoreboard_name + "=1..}]") + + "scoreboard players operation @s {} /= n60 {}".format( + sbn_pc + "TMinT", scoreboard_name + ) + ) + + result.append( + self.exeHead.format("@a[scores={" + scoreboard_name + "=1..}]") + + "scoreboard players operation @s {} = @s {}".format( + sbn_pc + "TSecT", scoreboard_name + ) + ) + result.append( + self.exeHead.format("@a[scores={" + scoreboard_name + "=1..}]") + + "scoreboard players operation @s {} /= n20 {}".format( + sbn_pc + "TSecT", scoreboard_name + ) + ) + result.append( + self.exeHead.format("@a[scores={" + scoreboard_name + "=1..}]") + + "scoreboard players operation @s {} %= n60 {}".format( + sbn_pc + "TSecT", scoreboard_name + ) + ) + + for i in range(pgs_style.count("_")): + npg_stl = ( + pgs_style.replace("_", progressbar[1][0], i + 1) + .replace("_", progressbar[1][1]) + .replace(r"%%N", self.midFileName) + if r"%%N" in pgs_style + else pgs_style.replace("_", progressbar[1][0], i + 1).replace( + "_", progressbar[1][1] + ) + ) + if r"%%s" in npg_stl: + npg_stl = npg_stl.replace( + r"%%s", + '"},{"score":{"name":"*","objective":"' + + scoreboard_name + + '"}},{"text":"', + ) + if r"%%%" in npg_stl: + npg_stl = npg_stl.replace( + r"%%%", + r'"},{"score":{"name":"*","objective":"' + + sbn_pc + + r'PercT"}},{"text":"%', + ) + if r"%%t" in npg_stl: + npg_stl = npg_stl.replace( + r"%%t", + r'"},{"score":{"name":"*","objective":"{-}TMinT"}},{"text":":"},' + r'{"score":{"name":"*","objective":"{-}TSecT"}},{"text":"'.replace( + r"{-}", sbn_pc + ), + ) + result.append( + self.exeHead.format( + r"@a[scores={" + + scoreboard_name + + f"={int(i * perEach)}..{math.ceil((i + 1) * perEach)}" + + r"}]" + ) + + r'titleraw @s actionbar {"rawtext":[{"text":"' + + npg_stl + + r'"}]}' + ) + + if r"%%%" in pgs_style: + result.append("scoreboard objectives remove {}PercT".format(sbn_pc)) + if r"%%t" in pgs_style: + result.append("scoreboard objectives remove {}TMinT".format(sbn_pc)) + result.append("scoreboard objectives remove {}TSecT".format(sbn_pc)) + + return result + + def _toCmdList_m1( + 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.debugMode: + 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.debugMode: + 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.debugMode: + 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.debugMode: + 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.debugMode: + 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.exeHead.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, + 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.debugMode: + 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.debugMode: + 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 speed: 速度,注意:这里的速度指的是播放倍率,其原理为在播放音频的时候,每个音符的播放时间除以 speed + :param player: 玩家选择器,默认为`@a` + :return: 全部指令列表[ ( str指令, int距离上一个指令的延迟 ),...] + """ + # :param volume: 音量,注意:这里的音量范围为(0,1],如果超出将被处理为正确值,其原理为在距离玩家 (1 / volume -1) 的地方播放音频 + tracks = {} + + if speed == 0: + if self.debugMode: + 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.exeHead.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.exeHead.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()) + + 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 speed: 速度,注意:这里的速度指的是播放倍率,其原理为在播放音频的时候,每个音符的播放时间除以 speed + :param player: 玩家选择器,默认为`@a` + :return: 全部指令列表[ ( str指令, int距离上一个指令的延迟 ),...] + """ + # :param volume: 音量,注意:这里的音量范围为(0,1],如果超出将被处理为正确值,其原理为在距离玩家 (1 / volume -1) 的地方播放音频 + tracks = {} + if speed == 0: + if self.debugMode: + 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.debugMode: + 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.debugMode: + 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.debugMode: + 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.exeHead.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.exeHead.format(player) + + f"playsound {soundID} @s ^ ^ ^{1 / MaxVolume - 1} {msg[2] / 128} " + f"{2 ** ((msg[1] - 60 - _X) / 12)}" + ] + + all_ticks = list(tracks.keys()) + + 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 to_mcpack( + self, + method: int = 1, + volume: float = 1.0, + speed: float = 1.0, + progressbar: Union[bool, tuple] = None, + scoreboard_name: str = "mscplay", + isAutoReset: 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失败原因) + """ + + # try: + cmdlist, maxlen, maxscore = self.methods[method - 1]( + scoreboard_name, volume, speed + ) + # except: + # return (False, f"无法找到算法ID{method}对应的转换算法") + + # 当文件f夹{self.outputPath}/temp/functions存在时清空其下所有项目,然后创建 + if os.path.exists(f"{self.outputPath}/temp/functions/"): + shutil.rmtree(f"{self.outputPath}/temp/functions/") + os.makedirs(f"{self.outputPath}/temp/functions/mscplay") + + # 写入manifest.json + if not os.path.exists(f"{self.outputPath}/temp/manifest.json"): + with open( + f"{self.outputPath}/temp/manifest.json", "w", encoding="utf-8" + ) 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}' + ) + else: + with open( + f"{self.outputPath}/temp/manifest.json", "r", encoding="utf-8" + ) 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", encoding="utf-8").write( + json.dumps(data) + ) + + # 将命令列表写入文件 + index_file = open( + f"{self.outputPath}/temp/functions/index.mcfunction", "w", encoding="utf-8" + ) + for track in cmdlist: + index_file.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)) + index_file.writelines( + ( + "scoreboard players add @a[scores={" + + scoreboard_name + + "=1..}] " + + scoreboard_name + + " 1\n", + ( + "scoreboard players reset @a[scores={" + + scoreboard_name + + "=" + + str(maxscore + 20) + + "..}]" + + f" {scoreboard_name}\n" + ) + if isAutoReset + else "", + f"function mscplay/progressShow\n" if progressbar else "", + ) + ) + + if progressbar: + if progressbar: + with open( + f"{self.outputPath}/temp/functions/mscplay/progressShow.mcfunction", + "w", + encoding="utf-8", + ) as f: + f.writelines( + "\n".join(self.__formProgressBar(maxscore, scoreboard_name)) + ) + else: + with open( + f"{self.outputPath}/temp/functions/mscplay/progressShow.mcfunction", + "w", + encoding="utf-8", + ) as f: + f.writelines( + "\n".join( + self.__formProgressBar( + maxscore, scoreboard_name, progressbar + ) + ) + ) + + index_file.close() + + if os.path.exists(f"{self.outputPath}/{self.midFileName}.mcpack"): + os.remove(f"{self.outputPath}/{self.midFileName}.mcpack") + compress_zipfile( + f"{self.outputPath}/temp/", f"{self.outputPath}/{self.midFileName}.mcpack" + ) + + shutil.rmtree(f"{self.outputPath}/temp/") + + return True, maxlen, maxscore + + def to_BDX_file( + self, + method: int = 1, + volume: float = 1.0, + speed: float = 1.0, + progressbar: Union[bool, tuple] = False, + scoreboard_name: str = "mscplay", + isAutoReset: bool = False, + author: str = "Eilles", + max_height: int = 64, + ): + """ + 使用method指定的转换算法,将midi转换为BDX结构文件 + :param method: 转换算法 + :param author: 作者名称 + :param progressbar: 进度条,(当此参数为True时使用默认进度条,为其他的值为真的参数时识别为进度条自定义参数,为其他值为假的时候不生成进度条) + :param max_height: 生成结构最大高度 + :param scoreboard_name: 我的世界的计分板名称 + :param volume: 音量,注意:这里的音量范围为(0,1],如果超出将被处理为正确值,其原理为在距离玩家 (1 / volume -1) 的地方播放音频 + :param speed: 速度,注意:这里的速度指的是播放倍率,其原理为在播放音频的时候,每个音符的播放时间除以 speed + :param isAutoReset: 是否自动重置计分板 + :return 成功与否,成功返回(True,未经过压缩的源,结构占用大小),失败返回(False,str失败原因) + """ + # try: + cmdlist, total_count, maxScore = self.methods[method - 1]( + scoreboard_name, volume, speed + ) + # except Exception as E: + # return (False, f"无法找到算法ID{method}对应的转换算法: {E}") + + if not os.path.exists(self.outputPath): + os.makedirs(self.outputPath) + + with open( + os.path.abspath(os.path.join(self.outputPath, f"{self.midFileName}.bdx")), + "w+", + ) as f: + f.write("BD@") + + _bytes = ( + b"BDX\x00" + + author.encode("utf-8") + + b" & Musicreater\x00\x01command_block\x00" + ) + + commands = [] + + for track in cmdlist: + commands += track + + if isAutoReset: + commands.append( + "scoreboard players reset @a[scores={" + + scoreboard_name + + "=" + + str(maxScore + 20) + + "}] " + + scoreboard_name, + ) + + cmdBytes, size, finalPos = to_BDX_bytes( + [(i, 0) for i in commands], max_height - 1 + ) + # 此处是对于仅有 True 的参数和自定义参数的判断 + if progressbar: + pgbBytes, pgbSize, pgbNowPos = to_BDX_bytes( + [ + (i, 0) + for i in ( + self.__formProgressBar(maxScore, scoreboard_name) + if progressbar + else self.__formProgressBar( + maxScore, scoreboard_name, progressbar + ) + ) + ], + max_height - 1, + ) + _bytes += pgbBytes + _bytes += move(y, -pgbNowPos[1]) + _bytes += move(z, -pgbNowPos[2]) + _bytes += move(x, 2) + + size[0] += 2 + pgbSize[0] + size[1] = max(size[1], pgbSize[1]) + size[2] = max(size[2], pgbSize[2]) + + _bytes += cmdBytes + + with open( + os.path.abspath(os.path.join(self.outputPath, f"{self.midFileName}.bdx")), + "ab+", + ) as f: + f.write(brotli.compress(_bytes + b"XE")) + + return True, total_count, maxScore, size, finalPos + + def to_BDX_file_with_delay( + self, + method: int = 1, + volume: float = 1.0, + speed: float = 1.0, + progressbar: Union[bool, tuple] = False, + player: str = "@a", + author: str = "Eilles", + max_height: int = 64, + ): + """ + 使用method指定的转换算法,将midi转换为BDX结构文件 + :param method: 转换算法 + :param author: 作者名称 + :param progressbar: 进度条,(当此参数为True时使用默认进度条,为其他的值为真的参数时识别为进度条自定义参数,为其他值为假的时候不生成进度条) + :param max_height: 生成结构最大高度 + :param volume: 音量,注意:这里的音量范围为(0,1],如果超出将被处理为正确值,其原理为在距离玩家 (1 / volume -1) 的地方播放音频 + :param speed: 速度,注意:这里的速度指的是播放倍率,其原理为在播放音频的时候,每个音符的播放时间除以 speed + :param player: 玩家选择器,默认为`@a` + :return 成功与否,成功返回(True,未经过压缩的源,结构占用大小),失败返回(False,str失败原因) + """ + + # try: + cmdlist, max_delay = self.methods_byDelay[method - 1]( + volume, + speed, + player, + ) + # except Exception as E: + # return (False, f"无法找到算法ID{method}对应的转换算法\n{E}") + + if not os.path.exists(self.outputPath): + os.makedirs(self.outputPath) + + with open( + os.path.abspath(os.path.join(self.outputPath, f"{self.midFileName}.bdx")), + "w+", + ) as f: + f.write("BD@") + + _bytes = ( + b"BDX\x00" + + author.encode("utf-8") + + b" & Musicreater\x00\x01command_block\x00" + ) + + # 此处是对于仅有 True 的参数和自定义参数的判断 + if progressbar: + progressbar = ( + r"▶ %%N [ %%s/%^s %%% __________ %%t|%^t ]", + ("§e=§r", "§7=§r"), + ) + + cmdBytes, size, finalPos = to_BDX_bytes(cmdlist, max_height - 1) + + if progressbar: + scb_name = self.midFileName[:5] + "Pgb" + _bytes += form_command_block_in_BDX_bytes( + r"scoreboard objectives add {} dummy {}播放用".replace(r"{}", scb_name), + 1, + customName="初始化进度条", + ) + _bytes += move(z, 2) + _bytes += form_command_block_in_BDX_bytes( + r"scoreboard players add {} {} 1".format(player, scb_name), + 1, + 1, + customName="显示进度条并加分", + ) + _bytes += move(y, 1) + pgbBytes, pgbSize, pgbNowPos = to_BDX_bytes( + [ + (i, 0) + for i in self.__formProgressBar(max_delay, scb_name, progressbar) + ], + max_height - 1, + ) + _bytes += pgbBytes + _bytes += move(y, -1 - pgbNowPos[1]) + _bytes += move(z, -2 - pgbNowPos[2]) + _bytes += move(x, 2) + _bytes += form_command_block_in_BDX_bytes( + r"scoreboard players reset {} {}".format(player, scb_name), + 1, + customName="置零进度条", + ) + _bytes += move(y, 1) + size[0] += 2 + pgbSize[0] + size[1] = max(size[1], pgbSize[1]) + size[2] = max(size[2], pgbSize[2]) + + size[1] += 1 + _bytes += cmdBytes + + with open( + os.path.abspath(os.path.join(self.outputPath, f"{self.midFileName}.bdx")), + "ab+", + ) as f: + f.write(brotli.compress(_bytes + b"XE")) + + return True, len(cmdlist), max_delay, size, finalPos + + def toDICT( + self, + ) -> dict: + """ + 使用金羿的转换思路,将midi转换为字典 + :return: dict() + """ + + # 一个midi中仅有16个通道 我们通过通道来识别而不是音轨 + channels = {} + 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: + 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 + else: + + 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, 距离演奏开始的毫秒)""" + + return channels diff --git a/msctPkgver/utils.py b/msctPkgver/utils.py new file mode 100644 index 0000000..229d99b --- /dev/null +++ b/msctPkgver/utils.py @@ -0,0 +1,222 @@ +import math +import os + +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 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 + """ + 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 下 无条件 + + :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 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 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)) + ) + 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 += key[x][1] + now_x += 1 + else: + + _bytes += key[z][int(z_forward)] + + else: + + _bytes += 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/poem.txt b/poem.txt new file mode 100644 index 0000000..d71b8ac --- /dev/null +++ b/poem.txt @@ -0,0 +1,47 @@ +> 是谁把科技的领域布满政治的火药 +> +> 是谁把纯净的蓝天染上暗淡的沉灰 +> +> 中国人民无不热爱自己伟大的祖国 +> +> 我们不会忘记屈辱历史留下的惨痛 +> +> 我们希望世界和平 +> +> 我们希望获得世界的尊重 +> +> 愿世上再也没有战争 +> +> 无论是热还是冷 +> +> 无论是经济还是政治 +> +> 让美妙的和平的优雅的音乐响彻世界 +> +> ——金羿 +> 2022 5 7 + + + +> Who has dropped political gunpowder into the technology +> +> Who has dyed clear blue sky into the dark grey +> +> All Chinese people love our great homeland +> +> We *WILL* remember the remain pain of the humiliating history +> +> We love the whole world but in peace +> +> We love everyone but under respect +> +> It is to be hoped that the war ends forever +> +> Whatever it is cold or hot +> +> Whatever it is economical or political +> +> Just let the wonderful music of peace surround the world +> +> ---- Eilles Wan +> 7/5 2022 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..04ba938 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +Brotli==1.0.9 +mido==1.2.10 \ No newline at end of file