Java版!MSQ流式解析初适配!

This commit is contained in:
EillesWan 2025-04-02 02:52:41 +08:00
parent 2df8d6a270
commit 889f8f9641
36 changed files with 786 additions and 61 deletions

3
.gitignore vendored
View File

@ -1,9 +1,10 @@
# sth. can't open
/msctPkgver/secrets/*.py
/msctPkgver/secrets/*.c
/fool/
# mystuff
/*.zip
/.vscode
/*.mid
/*.midi

View File

@ -6,7 +6,7 @@ Musicreater(音·创)
A free open source library used for dealing with **Minecraft** digital musics.
版权所有 © 2024 金羿 & 诸葛亮与八卦阵
Copyright © 2024 EillesWan & bgArray
Copyright © 2025 Eilles & bgArray
·本项目的协议颁发者为 金羿诸葛亮与八卦阵
The Licensor of Musicreater("this project") is Eilles Wan, bgArray.
@ -22,10 +22,10 @@ The Licensor of Musicreater("this project") is Eilles Wan, bgArray.
# 若需转载或借鉴 许可声明请查看仓库目录下的 License.md
__version__ = "2.2.2.2"
__vername__ = "接口小兼容"
__version__ = "2.2.3"
__vername__ = "Java版欲适配、初步MSQ流式适配"
__author__ = (
("金羿", "Eilles Wan"),
("金羿", "Eilles"),
("诸葛亮与八卦阵", "bgArray"),
("鱼旧梦", "ElapsingDreams"),
("偷吃不是Touch", "Touch"),

View File

@ -6,7 +6,7 @@
"""
版权所有 © 2024 金羿 & 诸葛亮与八卦阵
Copyright © 2024 EillesWan & bgArray
Copyright © 2025 Eilles & bgArray
开源相关声明请见 仓库根目录下的 License.md
Terms & Conditions: License.md in the root directory

View File

@ -6,7 +6,7 @@
"""
版权所有 © 2024 金羿 & 诸葛亮与八卦阵
Copyright © 2024 EillesWan & bgArray
Copyright © 2025 Eilles & bgArray
开源相关声明请见 仓库根目录下的 License.md
Terms & Conditions: License.md in the root directory
@ -58,6 +58,7 @@ class MidiDestroyedError(MSCTBaseException):
# super().__init__("未定义MidiFile对象你甚至没有对象就想要生孩子", *args)
# 此错误在本版本内已经不再使用
class CommandFormatError(RuntimeError):
"""指令格式与目标格式不匹配而引起的错误"""
@ -75,6 +76,10 @@ class CommandFormatError(RuntimeError):
# 这TM是什么错误
# 我什么时候写的这玩意?
# 我哪知道这说的是啥?
#
# 我知道这是什么了 —— 金羿 2025 0401
# 两个其他属性相同的音符在同一个通道,出现连续两个开音信息和连续两个停止信息
# 那么这两个音符的音长无法判断。这是个好问题,但是不是我现在能解决的,也不是我们现在想解决的问题
class NotDefineTempoError(MidiFormatException):
@ -109,7 +114,7 @@ class NoteOnOffMismatchError(MidiFormatException):
super().__init__("音符不匹配", *args)
class ZeroSpeedError(ZeroDivisionError):
class ZeroSpeedError(MSCTBaseException, ZeroDivisionError):
"""以0作为播放速度的错误"""
def __init__(self, *args):
@ -117,9 +122,26 @@ class ZeroSpeedError(ZeroDivisionError):
super().__init__("播放速度为0", *args)
class IllegalMinimumVolumeError(ValueError):
class IllegalMinimumVolumeError(MSCTBaseException, ValueError):
"""最小播放音量有误的错误"""
def __init__(self, *args):
"""最小播放音量错误"""
super().__init__("最小播放音量超出范围", *args)
super().__init__("最小播放音量超出范围", *args)
class MusicSequenceDecodeError(MSCTBaseException):
"""音乐序列解码错误"""
def __init__(self, *args):
"""音乐序列无法正确解码的错误"""
super().__init__("解码音符序列文件时出现问题", *args)
class MusicSequenceVerificationFailed(MusicSequenceDecodeError):
"""音乐序列校验失败"""
def __init__(self, *args):
"""音符序列文件与其校验值不一致"""
super().__init__("音符序列文件校验失败", *args)

View File

@ -6,7 +6,7 @@
"""
版权所有 © 2024 金羿 & 诸葛亮与八卦阵
Copyright © 2024 EillesWan & bgArray
Copyright © 2025 Eilles & bgArray
开源相关声明请见 仓库根目录下的 License.md
Terms & Conditions: License.md in the root directory
@ -29,6 +29,359 @@ from .main import (
from .types import Tuple, List, Dict, ChannelType
class FutureMidiConvertJavaE(MidiConvert):
def form_java_progress_bar(
self,
max_score: int,
scoreboard_name: str,
progressbar_style: ProgressBarStyle = DEFAULT_PROGRESSBAR_STYLE,
) -> List[MineCommand]:
"""
生成进度条
Parameters
----------
max_score: int
最大的积分值
scoreboard_name: str
所使用的计分板名称
progressbar_style: ProgressBarStyle
此参数详见 ../docs/库的生成与功能文档.md#进度条自定义
Returns
-------
list[MineCommand,]
"""
pgs_style = progressbar_style.base_style
"""用于被替换的进度条原始样式"""
"""
| 标识符 | 指定的可变量 |
|---------|----------------|
| `%%N` | 乐曲名(即传入的文件名)|
| `%%s` | 当前计分板值 |
| `%^s` | 计分板最大值 |
| `%%t` | 当前播放时间 |
| `%^t` | 曲目总时长 |
| `%%%` | 当前进度比率 |
| `_` | 用以表示进度条占位|
"""
perEach = max_score / pgs_style.count("_")
"""每个进度条代表的分值"""
result: List[MineCommand] = []
if r"%^s" in pgs_style:
pgs_style = pgs_style.replace(r"%^s", str(max_score))
if r"%^t" in pgs_style:
pgs_style = pgs_style.replace(r"%^t", mctick2timestr(max_score))
sbn_pc = scoreboard_name[:2]
if r"%%%" in pgs_style:
result.append(
MineCommand(
'scoreboard objectives add {}PercT dummy "百分比计算"'.format(
sbn_pc
),
annotation="新增临时百分比变量",
)
)
result.append(
MineCommand(
self.execute_cmd_head.format(
"@a[score_" + scoreboard_name + "_min=1]"
)
+ "scoreboard players set MaxScore {} {}".format(
scoreboard_name, max_score
),
annotation="设定音乐最大延迟分数",
)
)
result.append(
MineCommand(
self.execute_cmd_head.format(
"@a[score_" + scoreboard_name + "_min=1]"
)
+ "scoreboard players set n100 {} 100".format(scoreboard_name),
annotation="设置常量100",
)
)
result.append(
MineCommand(
self.execute_cmd_head.format(
"@a[scores_" + scoreboard_name + "_min=1]"
)
+ "scoreboard players operation @s {} = @s {}".format(
sbn_pc + "PercT", scoreboard_name
),
annotation="赋值临时百分比",
)
)
result.append(
MineCommand(
self.execute_cmd_head.format(
"@a[score_" + scoreboard_name + "_min=1]"
)
+ "scoreboard players operation @s {} *= n100 {}".format(
sbn_pc + "PercT", scoreboard_name
),
annotation="转换临时百分比之单位至%(扩大精度)",
)
)
result.append(
MineCommand(
self.execute_cmd_head.format(
"@a[score_" + scoreboard_name + "_min=1]"
)
+ "scoreboard players operation @s {} /= MaxScore {}".format(
sbn_pc + "PercT", scoreboard_name
),
annotation="计算百分比",
)
)
if r"%%t" in pgs_style:
result.append(
MineCommand(
'scoreboard objectives add {}TMinT dummy "时间计算:分"'.format(
sbn_pc
),
annotation="新增临时分变量",
)
)
result.append(
MineCommand(
'scoreboard objectives add {}TSecT dummy "时间计算:秒"'.format(
sbn_pc
),
annotation="新增临时秒变量",
)
)
result.append(
MineCommand(
self.execute_cmd_head.format(
"@a[score_" + scoreboard_name + "_min=1]"
)
+ "scoreboard players set n20 {} 20".format(scoreboard_name),
annotation="设置常量20",
)
)
result.append(
MineCommand(
self.execute_cmd_head.format(
"@a[score_" + scoreboard_name + "_min=1]"
)
+ "scoreboard players set n60 {} 60".format(scoreboard_name),
annotation="设置常量60",
)
)
result.append(
MineCommand(
self.execute_cmd_head.format(
"@a[score_" + scoreboard_name + "_min=1]"
)
+ "scoreboard players operation @s {} = @s {}".format(
sbn_pc + "TMinT", scoreboard_name
),
annotation="赋值临时分",
)
)
result.append(
MineCommand(
self.execute_cmd_head.format(
"@a[score_" + scoreboard_name + "_min=1]"
)
+ "scoreboard players operation @s {} /= n20 {}".format(
sbn_pc + "TMinT", scoreboard_name
),
annotation="转换临时分之单位为秒(缩减精度)",
)
)
result.append(
MineCommand(
self.execute_cmd_head.format(
"@a[score_" + scoreboard_name + "_min=1]"
)
+ "scoreboard players operation @s {} = @s {}".format(
sbn_pc + "TSecT", sbn_pc + "TMinT"
),
annotation="赋值临时秒",
)
)
result.append(
MineCommand(
self.execute_cmd_head.format(
"@a[score_" + scoreboard_name + "_min=1]"
)
+ "scoreboard players operation @s {} /= n60 {}".format(
sbn_pc + "TMinT", scoreboard_name
),
annotation="转换临时分之单位为分(缩减精度)",
)
)
result.append(
MineCommand(
self.execute_cmd_head.format(
"@a[score_" + scoreboard_name + "_min=1]"
)
+ "scoreboard players operation @s {} %= n60 {}".format(
sbn_pc + "TSecT", scoreboard_name
),
annotation="确定临时秒(框定精度区间)",
)
)
for i in range(pgs_style.count("_")):
npg_stl = (
pgs_style.replace("_", progressbar_style.played_style, i + 1)
.replace("_", progressbar_style.to_play_style)
.replace(r"%%N", self.music_name)
.replace(
r"%%s",
'"},{"score":{"name":"*","objective":"'
+ scoreboard_name
+ '"}},{"text":"',
)
.replace(
r"%%%",
r'"},{"score":{"name":"*","objective":"'
+ sbn_pc
+ r'PercT"}},{"text":"%',
)
.replace(
r"%%t",
r'"},{"score":{"name":"*","objective":"{-}TMinT"}},{"text":":"},'
r'{"score":{"name":"*","objective":"{-}TSecT"}},{"text":"'.replace(
r"{-}", sbn_pc
),
)
)
result.append(
MineCommand(
self.execute_cmd_head.format(
f"@a[score_{scoreboard_name}_min={int(i * perEach)},score_{scoreboard_name}={math.ceil((i + 1) * perEach)}]"
)
+ r'titleraw @s actionbar {"rawtext":[{"text":"'
+ npg_stl
+ r'"}]}',
annotation="进度条显示",
)
)
if r"%%%" in pgs_style:
result.append(
MineCommand(
"scoreboard objectives remove {}PercT".format(sbn_pc),
annotation="移除临时百分比变量",
)
)
if r"%%t" in pgs_style:
result.append(
MineCommand(
"scoreboard objectives remove {}TMinT".format(sbn_pc),
annotation="移除临时分变量",
)
)
result.append(
MineCommand(
"scoreboard objectives remove {}TSecT".format(sbn_pc),
annotation="移除临时秒变量",
)
)
self.progress_bar_command = result
return result
def to_command_list_in_java_score(
self,
scoreboard_name: str = "mscplay",
source_of_sound: str = "ambient",
) -> Tuple[List[List[MineCommand]], int, int]:
"""
将midi转换为 Java 1.12.2 版我的世界命令列表
Parameters
----------
scoreboard_name: str
我的世界的计分板名称
Returns
-------
tuple( list[list[MineCommand指令,... ],... ], int指令数量, int音乐时长游戏刻 )
"""
command_channels = []
command_amount = 0
max_score = 0
# 此处 我们把通道视为音轨
for channel in self.channels.values():
# 如果当前通道为空 则跳过
if not channel:
continue
this_channel = []
for note in channel:
max_score = max(max_score, note.start_tick)
(
mc_sound_ID,
relative_coordinates,
volume_percentage,
mc_pitch,
) = minenote_to_command_paramaters(
note,
pitch_deviation=self.music_deviation,
)
this_channel.append(
MineCommand(
(
self.execute_cmd_head.format(
"@a[score_{0}_min={1},score_{0}={1}]".format(
scoreboard_name, note.start_tick
)
.replace("(", r"{")
.replace(")", r"}")
)
+ "playsound minecraft:block.{} {} @s ~{} ~{} ~{} {} {} {}".format(
mc_sound_ID,
source_of_sound,
*relative_coordinates,
volume_percentage,
1.0 if note.percussive else mc_pitch,
self.minimum_volume,
)
),
annotation=(
"{}播放噪音{}".format(
mctick2timestr(note.start_tick),
mc_sound_ID,
)
if note.percussive
else "{}播放乐音{}".format(
mctick2timestr(note.start_tick),
"{}:{:.2f}".format(mc_sound_ID, mc_pitch),
)
),
),
)
command_amount += 1
if this_channel:
self.music_command_list.extend(this_channel)
command_channels.append(this_channel)
return command_channels, command_amount, max_score
class FutureMidiConvertRSNB(MidiConvert):
"""
加入红石音乐适配
@ -217,7 +570,7 @@ class FutureMidiConvertM5(MidiConvert):
if not track:
continue
note_queue = empty_midi_channels(staff=[])
note_queue = empty_midi_channels(default_staff=[])
for msg in track:
if msg.time != 0:

View File

@ -23,7 +23,7 @@ Musicreater (音·创)
A free open source library used for convert midi file into formats that is suitable for **Minecraft**.
版权所有 © 2024 金羿 & 诸葛亮与八卦阵
Copyright © 2024 EillesWan & bgArray
Copyright © 2025 Eilles & bgArray
开源相关声明请见 ../License.md
Terms & Conditions: ../License.md

View File

@ -8,7 +8,7 @@ Musicreater (音·创)
A free open source library used for dealing with **Minecraft** digital musics.
版权所有 © 2024 金羿 & 诸葛亮与八卦阵
Copyright © 2024 EillesWan & bgArray
Copyright © 2025 Eilles & bgArray
·本项目的协议颁发者为 金羿诸葛亮与八卦阵
The Licensor of Musicreater("this project") is Eilles Wan, bgArray.
@ -30,10 +30,11 @@ The Licensor of Musicreater("this project") is Eilles Wan, bgArray.
# 赶快呼叫 程序员Let's Go 直ぐに呼びましょプログラマ レッツゴー! Hurry to call the programmer! Let's Go!
import math
import os
import math
import mido
from xxhash import xxh3_64, xxh3_128
from .constants import *
from .exceptions import *
@ -203,6 +204,7 @@ class MusicSequence:
) = cls.to_music_note_channels(
midi=mido_file,
speed=speed_multiplier,
default_program_value=-1, # TODO 默认音色可调
pitched_note_rtable=pitched_note_referance_table,
percussion_note_rtable=percussion_note_referance_table,
default_tempo_value=default_tempo,
@ -226,6 +228,7 @@ class MusicSequence:
def load_decode(
cls,
bytes_buffer_in: bytes,
verify: bool = True,
):
"""从字节码导入音乐序列"""
@ -238,8 +241,16 @@ class MusicSequence:
music_name_ = bytes_buffer_in[8 : (stt_index := 8 + (group_1 >> 10))].decode(
"GB18030"
)
channels_: MineNoteChannelType = empty_midi_channels(staff=[])
channels_: MineNoteChannelType = empty_midi_channels(default_staff=[])
total_note_count = 0
if verify:
_header_index = stt_index
_total_verify_code = 0
for channel_index in channels_.keys():
channel_note_count = 0
_channel_start_index = stt_index
for i in range(
int.from_bytes(
bytes_buffer_in[stt_index : (stt_index := stt_index + 4)], "big"
@ -258,10 +269,70 @@ class MusicSequence:
is_high_time_precision=high_quantity,
)
)
channel_note_count += 1
stt_index = end_index
except:
except Exception as _err:
print(channels_)
raise
raise MusicSequenceDecodeError(_err)
if verify:
if (
_count_verify := xxh3_64(
channel_note_count.to_bytes(4, "big", signed=False),
seed=3,
)
).digest() != (
_original_code := bytes_buffer_in[stt_index : stt_index + 8]
):
raise MusicSequenceVerificationFailed(
"通道 {} 音符数量校验失败:{} -> `{}`;原始为 `{}`".format(
channel_index,
channel_note_count,
_count_verify.digest(),
_original_code,
)
)
if (
_channel_verify := xxh3_64(
bytes_buffer_in[_channel_start_index:stt_index],
seed=channel_note_count,
)
).digest() != (
_original_code := bytes_buffer_in[stt_index + 8 : stt_index + 16]
):
raise MusicSequenceVerificationFailed(
"通道 {} 音符数据校验失败:`{}`;原始为 `{}`".format(
channel_index,
_channel_verify.digest(),
_original_code,
)
)
_total_verify_code ^= (
_count_verify.intdigest() ^ _channel_verify.intdigest()
)
total_note_count += channel_note_count
stt_index += 16
if verify:
if (
_total_verify_res := xxh3_128(
_total_verify := (
xxh3_64(
bytes_buffer_in[0:_header_index],
seed=total_note_count,
).intdigest()
^ _total_verify_code
).to_bytes(8, "big"),
seed=total_note_count,
).digest()
) != (_original_code := bytes_buffer_in[stt_index:]):
raise MusicSequenceVerificationFailed(
"全曲最终校验失败。全曲音符数:{},全曲校验码异或和:`{}` -> `{}`;原始为 `{}`".format(
total_note_count,
_total_verify,
_total_verify_res,
_original_code,
)
)
return cls(
name_of_music=music_name_,
@ -280,6 +351,7 @@ class MusicSequence:
) -> bytes:
"""将音乐序列转为二进制字节码"""
# (已废弃)
# 第一版的码头: MSQ# 字串编码: UTF-8
# 第一版格式
# 音乐名称长度 6 位 支持到 63
@ -306,8 +378,11 @@ class MusicSequence:
# for note_ in note_list:
# bytes_buffer += note_.encode()
# (已废弃)
# 第二版的码头: MSQ@ 字串编码: GB18030
# 第三版的码头: MSQ! 字串编码: GB18030 大端字节序
# 音乐名称长度 6 位 支持到 63
# 最小音量 minimum_volume 10 位 最大支持 1023 即三位小数
# 共 16 位 合 2 字节
@ -316,33 +391,73 @@ class MusicSequence:
# 总音调偏移 music_deviation 15 位 最大支持 -16383 ~ 16383 即 三位小数
# 共 16 位 合 2 字节
# +++
# 音乐名称 music_name 长度最多63 支持到 31 个中文字符 或 63 个西文字符
# 音乐名称 music_name 长度最多 63 支持到 31 个中文字符 或 63 个西文字符
bytes_buffer = (
b"MSQ@"
b"MSQ!"
+ (
(len(r := self.music_name.encode("GB18030")) << 10) # 音乐名称长度
+ round(self.minimum_volume * 1000) # 最小音量
(len(r := self.music_name.encode("GB18030")) << 10) # 音乐名称长度
+ round(self.minimum_volume * 1000) # 最小音量
).to_bytes(2, "big")
+ (
(
(
(high_time_precision << 1) # 是否启用“高精度”音符时间控制
+ (1 if (k := round(self.music_deviation * 1000)) < 0 else 0) # 总音调偏移的正负位
+ (
1 if (k := round(self.music_deviation * 1000)) < 0 else 0
) # 总音调偏移的正负位
)
<< 14
)
+ abs(k) # 总音调偏移
+ abs(k) # 总音调偏移
).to_bytes(2, "big", signed=False)
+ r
)
# 此上是音符序列的元信息,接下来是多通道的音符序列
# 每个通道的开头是 32 位的 序列长度 共 4 字节
# 接下来根据这个序列的长度来读取音符数据
# 若启用“高精度”,则每个音符皆添加一个字节,用于存储音符时间控制精度偏移
# 此值每增加 1则音符向后播放时长增加 1/1250 秒
# 高精度功能在 MineNote 类实现
# (第三版新增)每个通道结尾包含一个 128 位的 XXHASH 校验值,用以标识该通道结束
# 在这 128 位里,前 64 位是该通道音符数的 XXHASH64 校验值,以 3 作为种子值
# 后 64 位是整个通道全部字节串的 XXHASH64 校验值(包括通道开头的音符数),以 该通道音符数 作为种子值
_final_hash_codec = xxh3_64(
bytes_buffer, seed=self.total_note_count
).intdigest()
for channel_index, note_list in self.channels.items():
bytes_buffer += len(note_list).to_bytes(4, "big")
channel_buffer = len_buffer = len(note_list).to_bytes(
4, "big", signed=False
)
for note_ in note_list:
bytes_buffer += note_.encode(is_high_time_precision=high_time_precision)
channel_buffer += note_.encode(
is_high_time_precision=high_time_precision
)
_now_hash_codec_spliter = xxh3_64(len_buffer, seed=3)
_now_hash_codec_verifier = xxh3_64(
channel_buffer, seed=int.from_bytes(len_buffer, "big", signed=False)
)
bytes_buffer += channel_buffer
bytes_buffer += (
_now_hash_codec_spliter.digest() + _now_hash_codec_verifier.digest()
)
_final_hash_codec ^= (
_now_hash_codec_spliter.intdigest()
^ _now_hash_codec_verifier.intdigest()
)
# 在所有音符通道表示完毕之后,由一个 128 位的 XXHASH 校验值,用以标识文件结束并校验
# 该 128 位的校验值是对于前述所有校验值的异或所得值之 XXHASH128 校验值,以 全曲音符总数 作为种子值
bytes_buffer += xxh3_128(
_final_hash_codec.to_bytes(8, "big"), seed=self.total_note_count
).digest()
return bytes_buffer
@ -378,6 +493,7 @@ class MusicSequence:
midi: mido.MidiFile,
ignore_mismatch_error: bool = True,
speed: float = 1.0,
default_program_value: int = -1,
default_tempo_value: int = mido.midifiles.midifiles.DEFAULT_TEMPO,
pitched_note_rtable: MidiInstrumentTableType = MM_TOUCH_PITCHED_INSTRUMENT_TABLE,
percussion_note_rtable: MidiInstrumentTableType = MM_TOUCH_PERCUSSION_INSTRUMENT_TABLE,
@ -392,8 +508,10 @@ class MusicSequence:
需要处理的midi对象
speed: float
音乐播放速度倍数
default_program_value: int
默认的 MIDI 乐器值
default_tempo_value: int
默认的MIDI TEMPO值
默认的 MIDI TEMPO
pitched_note_rtable: Dict[int, Tuple[str, int]]
乐音乐器Midi-MC对照表
percussion_note_rtable: Dict[int, Tuple[str, int]]
@ -411,8 +529,10 @@ class MusicSequence:
raise ZeroSpeedError("播放速度为 0 ,其需要(0,1]范围内的实数。")
# 一个midi中仅有16个通道 我们通过通道来识别而不是音轨
midi_channels: MineNoteChannelType = empty_midi_channels(staff=[])
channel_program: Dict[int, int] = empty_midi_channels(staff=-1)
midi_channels: MineNoteChannelType = empty_midi_channels(default_staff=[])
channel_program: Dict[int, int] = empty_midi_channels(
default_staff=default_program_value
)
tempo = default_tempo_value
note_count = 0
note_count_per_instrument: Dict[str, int] = {}
@ -426,7 +546,7 @@ class MusicSequence:
int,
]
],
] = empty_midi_channels(staff=[])
] = empty_midi_channels(default_staff=[])
note_queue_B: Dict[
int,
List[
@ -435,7 +555,7 @@ class MusicSequence:
int,
]
],
] = empty_midi_channels(staff=[])
] = empty_midi_channels(default_staff=[])
# 直接使用mido.midifiles.tracks.merge_tracks转为单轨
# 采用的时遍历信息思路
@ -957,7 +1077,9 @@ class MidiConvert(MusicSequence):
self.progress_bar_command = result
return result
def redefine_execute_format(self, is_old_exe_cmd_using: bool = False):
def redefine_execute_format(
self, is_old_exe_cmd_using: bool = False
) -> "MidiConvert":
"""
根据是否使用旧版执行命令格式重新定义执行命令的起始格式

View File

@ -3,7 +3,7 @@
存放非音·创本体的附加功能件
版权所有 © 2024 金羿 & 诸葛亮与八卦阵
Copyright © 2024 EillesWan & bgArray
Copyright © 2025 Eilles & bgArray
开源相关声明请见 仓库根目录下的 License.md
Terms & Conditions: License.md in the root directory

View File

@ -3,7 +3,7 @@
用以生成附加包的附加功能
版权所有 © 2024 金羿 & 诸葛亮与八卦阵
Copyright © 2024 EillesWan & bgArray
Copyright © 2025 Eilles & bgArray
开源相关声明请见 仓库根目录下的 License.md
Terms & Conditions: License.md in the root directory

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
"""
版权所有 © 2024 金羿 & 诸葛亮与八卦阵
Copyright © 2024 EillesWan & bgArray
Copyright © 2025 Eilles & bgArray
开源相关声明请见 仓库根目录下的 License.md
Terms & Conditions: License.md in the root directory

View File

@ -6,7 +6,7 @@
"""
版权所有 © 2024 金羿 & 诸葛亮与八卦阵
Copyright © 2024 EillesWan & bgArray
Copyright © 2025 Eilles & bgArray
开源相关声明请见 仓库根目录下的 License.md
Terms & Conditions: License.md in the root directory

View File

@ -5,7 +5,7 @@
"""
版权所有 © 2024 金羿 & 诸葛亮与八卦阵
Copyright © 2024 EillesWan & bgArray
Copyright © 2025 Eilles & bgArray
开源相关声明请见 仓库根目录下的 License.md
Terms & Conditions: License.md in the root directory

View File

@ -3,7 +3,7 @@
用以生成BDX结构文件的附加功能
版权所有 © 2024 金羿 & 诸葛亮与八卦阵
Copyright © 2024 EillesWan & bgArray
Copyright © 2025 Eilles & bgArray
开源相关声明请见 仓库根目录下的 License.md
Terms & Conditions: License.md in the root directory

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
"""
版权所有 © 2024 金羿 & 诸葛亮与八卦阵
Copyright © 2024 EillesWan & bgArray
Copyright © 2025 Eilles & bgArray
开源相关声明请见 仓库根目录下的 License.md
Terms & Conditions: License.md in the root directory

View File

@ -5,7 +5,7 @@
"""
版权所有 © 2024 金羿 & 诸葛亮与八卦阵
Copyright © 2024 EillesWan & bgArray
Copyright © 2025 Eilles & bgArray
开源相关声明请见 仓库根目录下的 License.md
Terms & Conditions: License.md in the root directory

View File

@ -5,7 +5,7 @@
"""
版权所有 © 2024 金羿 & 诸葛亮与八卦阵
Copyright © 2024 EillesWan & bgArray
Copyright © 2025 Eilles & bgArray
开源相关声明请见 仓库根目录下的 License.md
Terms & Conditions: License.md in the root directory

View File

@ -3,7 +3,7 @@
用以生成单个mcstructure文件的附加功能
版权所有 © 2024 金羿 & 诸葛亮与八卦阵
Copyright © 2024 EillesWan & bgArray
Copyright © 2025 Eilles & bgArray
开源相关声明请见 仓库根目录下的 License.md
Terms & Conditions: License.md in the root directory

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
"""
版权所有 © 2024 金羿 & 诸葛亮与八卦阵
Copyright © 2024 EillesWan & bgArray
Copyright © 2025 Eilles & bgArray
开源相关声明请见 仓库根目录下的 License.md
Terms & Conditions: License.md in the root directory

View File

@ -5,7 +5,7 @@
"""
版权所有 © 2024 金羿 & 诸葛亮与八卦阵
Copyright © 2024 EillesWan & bgArray
Copyright © 2025 Eilles & bgArray
开源相关声明请见 仓库根目录下的 License.md
Terms & Conditions: License.md in the root directory

View File

@ -5,7 +5,7 @@
"""
版权所有 © 2024 金羿 & 诸葛亮与八卦阵
Copyright © 2024 EillesWan & bgArray
Copyright © 2025 Eilles & bgArray
开源相关声明请见 仓库根目录下的 License.md
Terms & Conditions: License.md in the root directory

View File

@ -5,7 +5,7 @@
"""
版权所有 © 2024 金羿 & 诸葛亮与八卦阵
Copyright © 2024 EillesWan & bgArray
Copyright © 2025 Eilles & bgArray
开源相关声明请见 仓库根目录下的 License.md
Terms & Conditions: License.md in the root directory

View File

@ -3,7 +3,7 @@
用以生成Schematic结构的附加功能
版权所有 © 2024 金羿 & 诸葛亮与八卦阵
Copyright © 2024 EillesWan & bgArray
Copyright © 2025 Eilles & bgArray
开源相关声明请见 仓库根目录下的 License.md
Terms & Conditions: License.md in the root directory

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
"""
版权所有 © 2024 金羿 & 诸葛亮与八卦阵
Copyright © 2024 EillesWan & bgArray
Copyright © 2025 Eilles & bgArray
开源相关声明请见 仓库根目录下的 License.md
Terms & Conditions: License.md in the root directory

View File

@ -3,7 +3,7 @@
用以启动WebSocket服务器播放的附加功能
版权所有 © 2024 金羿 & 诸葛亮与八卦阵
Copyright © 2024 EillesWan & bgArray
Copyright © 2025 Eilles & bgArray
开源相关声明请见 仓库根目录下的 License.md
Terms & Conditions: License.md in the root directory

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
"""
版权所有 © 2024 金羿 & 诸葛亮与八卦阵
Copyright © 2024 EillesWan & bgArray
Copyright © 2025 Eilles & bgArray
开源相关声明请见 仓库根目录下的 License.md
Terms & Conditions: License.md in the root directory

View File

@ -6,7 +6,7 @@
"""
版权所有 © 2024 金羿 & 诸葛亮与八卦阵
Copyright © 2024 EillesWan & bgArray
Copyright © 2025 Eilles & bgArray
开源相关声明请见 仓库根目录下的 License.md
Terms & Conditions: License.md in the root directory

View File

@ -6,7 +6,7 @@
"""
版权所有 © 2024 金羿 & 诸葛亮与八卦阵
Copyright © 2024 EillesWan & bgArray
Copyright © 2025 Eilles & bgArray
开源相关声明请见 仓库根目录下的 License.md
Terms & Conditions: License.md in the root directory

View File

@ -5,7 +5,7 @@
"""
版权所有 © 2024 金羿 & 诸葛亮与八卦阵
Copyright © 2024 EillesWan & bgArray
Copyright © 2025 Eilles & bgArray
开源相关声明请见 仓库根目录下的 License.md
Terms & Conditions: License.md in the root directory
@ -38,7 +38,9 @@ from .types import (
)
def empty_midi_channels(channel_count: int = 17, staff: Any = {}) -> Dict[int, Any]:
def empty_midi_channels(
channel_count: int = 17, default_staff: Any = {}
) -> Dict[int, Any]:
"""
空MIDI通道字典
"""
@ -46,7 +48,11 @@ def empty_midi_channels(channel_count: int = 17, staff: Any = {}) -> Dict[int, A
return dict(
(
i,
(staff.copy() if isinstance(staff, (dict, list)) else staff),
(
default_staff.copy()
if isinstance(default_staff, (dict, list))
else default_staff
),
) # 这告诉我们你不能忽略任何一个复制的序列因为它真的我哭死折磨我一整天全在这个bug上了
for i in range(channel_count)
)

View File

@ -91,6 +91,7 @@
- 感谢 **小埋**\<QQ2039310975\> 反馈附加包生成时缺少描述和标题的问题
- <table><tr><td>感谢 **油炸**&lt;QQ2836146704&gt; 激励我们不断开发新的内容</td><td><img height="50" src="https://foruda.gitee.com/images/1695478907647543027/08ea9909_9911226.jpeg"></td></tr></table>
- 感谢 ****\<QQ237667809\> 反馈在新版本的指令格式下计分板播放器的附加包无法播放的问题
- 感谢 **梦幻duang**\<QQ13753593\> 为我们提供 Java 1.12.2 版本命令格式参考
> 感谢广大群友为此库提供的测试和建议等
>

3
docs/FSQ文件格式.md Normal file
View File

@ -0,0 +1,3 @@
# FSQ 文件格式
还在设计

View File

@ -2,18 +2,25 @@
MSQ 文件是 · 存储音符序列的一种格式取自 MusicSeQuence
## MSQ 第二版
现在 · 及其上游软件使用的是在 第二版 的基础上增设验证功能的 MSQ 第三版
第二版的码头是 MSQ@ 这一版中所有的**字符串** _**GB18030**_ 编码进行编解码**数值****_大端序_**存储
## MSQ 第三版
码头是文件前四个字节的内容这一部分内容是可读的 ASCII 字串因此第二版的文件前四个字节的内容必为 MSQ@
第二版的码头是 `MSQ@` 这一版中所有的**字符串** _**GB18030**_ 编码进行编解码**数值****_大端序_**存储
MSQ 第三版的码头是 `MSQ!`
码头是文件前四个字节的内容这一部分内容是可读的 ASCII 字串因此第三版的文件前四个字节的内容必为 `MSQ!`
MSQ@ 是因为美式键盘上 @ Shift+2 按下取得的故代表 MSQ 第二版
你猜为什么第三版是 `MSQ!`
### 元信息
| 信息名称 | 西文代号 | 位长多少个 0 1 | 支持说明 |
| ------------------------------ | -------------------------- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **码头** | _无_ | 32 | 值为 `MSQ!` |
| **音乐名称长度** | music_name_length | 6 | 支持数值 0~63 |
| **最小音量** | minimum_volume | 10 | 支持数值 0~1023注意这里每个 1 代表最小音量的 0.001 个单位即取值是此处表示数字的千分倍 |
| **是否启用高精度音符时间控制** | enable_high_precision_time | 1 | 1 是启用反之同理 |
@ -39,4 +46,58 @@ MSQ 文件是 音·创 存储音符序列的一种格式,取自 MusicSeQuence
| **是否启用声像位移** | is_displacement_included | 1 | 1 是启用反之同理 |
| **时间精度提升值**非必含 | high_time_precision | 8 | 支持数值 0~255若在 元信息 中启用**高精度音符时间控制**则此值启用代表音符时间控制精度偏移此值每增加 1则音符开始时刻向后增加 1/1250 |
| **乐器名称** | sound_name | 依据先前定义 | 最多可支持 31 个中文字符 63 个西文字符其长度取决于先前获知的 乐器名称长度 的定义 |
| **声像位移**非必含 | position_displacement | 共三个值每个值 16 48 | 若前述**是否启用声像位移**已启用则此值启用三个值分别代表 xyz 轴上的便宜每个值支持数值 0~65535注意这里每个 1 代表最小音量的 0.001 个单位即取值是此处表示数字的千分倍 |
| **声像位移**非必含 | position_displacement | 共三个值每个值 16 48 | 若前述**是否启用声像位移**已启用则此值启用三个值分别代表 xyz 轴上的偏移每个值支持数值 0~65535注意这里每个 1 代表最小音量的 0.001 个单位即取值是此处表示数字的千分倍 |
### 序列验证
_第三版新增_
在每个音符序列结尾包含一个 128 位的校验值用以标识该序列结束的同时验证该序列的完整性
在这 128 位里 64 位是该通道音符数的 XXHASH64 校验值 3 作为种子值
64 位是整个通道全部字节串的 XXHASH64 校验值包括通道开头的音符数 该通道音符数 作为种子值
### 文件验证
_第三版新增_
在所有有效数据之后包含一个 128 位的校验值用以标识整个文件结束的同时验证整个文件的完整性
128 位的校验值是 包括码头在内的元信息的 XXHASH64 校验值种子值是全曲音符数 对于前述所有校验值彼此异或的异或 所得值之 XXHASH128 校验值 全曲音符总数 作为种子值
请注意是前述每个 XXHASH64 校验值的异或每次取 XXHASH64 都计一遍也就并非是每个序列结尾那个已经合并了的 128 位校验值再彼此异或对于这个异或值再取其种子是 全曲音符数 XXHASH128 校验字节码
听起来很复杂我来举个例子以下是该算法的伪代码我们设
- `meta_info` : `bytes` 元信息字节串
- `note_seq_1` : `bytes` 第一个音符序列的编码字节串
- `note_seq_2` : `bytes` 第二个音符序列的编码字节串
- `XXH64(bytes, seed)` : `bytes` XXHASH64 校验函数
- `XXH128(bytes, seed)` : `bytes` XXHASH128 校验函数
- `XOR(bytesLike, bytesLike)` : `bytes` 异或 函数
- `note_count` : `int` 全曲音符数
- `seq_1_note_count` : `int` 第一个音符序列的音符数
- `seq_2_note_count` : `int` 第二个音符序列的音符数
为了简化我们假设只有两个序列实际上每个通道都是一个序列最多 16 个序列
那么一个完整的 MSQ 文件应当如下排列其字节串
```assembly
ADD meta_info
ADD note_seq_1
ADD XXH64(seq_1_note_count, 3)
ADD XXH64(note_seq_1, seq_1_note_count)
ADD note_seq_2
ADD XXH64(seq_2_note_count, 3)
ADD XXH64(note_seq_2, seq_2_note_count)
ADD XXH128(
XOR(
XXH64(meta_info, note_count),
XOR(
XXH64(seq_1_note_count, 3),
XXH64(note_seq_1, seq_1_note_count)
),
),
note_count
)
```

View File

@ -13,7 +13,7 @@
**_使用时请遵循协议规定_**
- 版权所有 © 2024 金羿 & 诸葛亮与八卦阵
- Copyright © 2024 EillesWan & bgArray
- Copyright © 2025 Eilles & bgArray
* 开源相关声明请见 仓库根目录下的 License.md
* Terms & Conditions: License.md in the root directory

View File

@ -10,7 +10,7 @@ Musicreater (音·创)
A free open source library used for convert midi file into formats that is suitable for **Minecraft**.
版权所有 © 2024 金羿 & 诸葛亮与八卦阵
Copyright © 2024 EillesWan & bgArray
Copyright © 2025 Eilles & bgArray
开源相关声明请见 ./License.md
Terms & Conditions: ./License.md

14
example_msq_opera.py Normal file
View File

@ -0,0 +1,14 @@
import Musicreater
msc_seq = Musicreater.MusicSequence.from_mido(
Musicreater.mido.MidiFile("./resources/测试片段.mid",),
"TEST-测试片段",
)
with open("test.msq","wb") as f:
f.write(msq_bytes := msc_seq.encode_dump())
with open("test.msq","rb") as f:
msc_seq_r = Musicreater.MusicSequence.load_decode(f.read())
print(msc_seq_r)

141
let_future_java.py Normal file
View File

@ -0,0 +1,141 @@
"""
版权所有 © 2024 金羿 & 诸葛亮与八卦阵
Copyright © 2025 Eilles & bgArray
开源相关声明请见 仓库根目录下的 License.md
Terms & Conditions: License.md in the root directory
"""
import os
import shutil
from typing import Optional, Tuple
import Musicreater.experiment
from Musicreater.plugin.archive import compress_zipfile
def to_zip_pack_in_score(
midi_cvt: Musicreater.experiment.FutureMidiConvertJavaE,
dist_path: str,
progressbar_style: Optional[Musicreater.experiment.ProgressBarStyle],
scoreboard_name: str = "mscplay",
sound_source: str = "ambient",
auto_reset: bool = False,
) -> Tuple[int, int]:
"""
将midi以计分播放器形式转换为我的世界函数附加包
Parameters
----------
midi_cvt: MidiConvert 对象
用于转换的MidiConvert对象
dist_path: str
转换结果输出的目标路径
progressbar_style: ProgressBarStyle 对象
进度条对象
scoreboard_name: str
我的世界的计分板名称
auto_reset: bool
是否自动重置计分板
Returns
-------
tuple[int指令数量, int音乐总延迟]
"""
cmdlist, maxlen, maxscore = midi_cvt.to_command_list_in_java_score(
scoreboard_name=scoreboard_name,
source_of_sound=sound_source,
)
# 当文件f夹{self.outputPath}/temp/mscplyfuncs存在时清空其下所有项目然后创建
if os.path.exists(f"{dist_path}/temp/mscplyfuncs/"):
shutil.rmtree(f"{dist_path}/temp/mscplyfuncs/")
os.makedirs(f"{dist_path}/temp/mscplyfuncs/mscplay")
# 写入stop.mcfunction
with open(
f"{dist_path}/temp/mscplyfuncs/stop.mcfunction", "w", encoding="utf-8"
) as f:
f.write("scoreboard players reset @a {}".format(scoreboard_name))
# 将命令列表写入文件
index_file = open(
f"{dist_path}/temp/mscplyfuncs/index.mcfunction", "w", encoding="utf-8"
)
for i in range(len(cmdlist)):
index_file.write(f"function mscplyfuncs:mscplay/track{i + 1}\n")
with open(
f"{dist_path}/temp/mscplyfuncs/mscplay/track{i + 1}.mcfunction",
"w",
encoding="utf-8",
) as f:
f.write("\n".join([single_cmd.cmd for single_cmd in cmdlist[i]]))
index_file.writelines(
(
"scoreboard players add @a[score_{0}_min=1] {0} 1\n".format(
scoreboard_name
),
(
"scoreboard players reset @a[score_{0}_min={1}] {0}\n".format(
scoreboard_name, maxscore + 20
)
if auto_reset
else ""
),
f"function mscplyfuncs:mscplay/progressShow\n" if progressbar_style else "",
)
)
if progressbar_style:
with open(
f"{dist_path}/temp/mscplyfuncs/mscplay/progressShow.mcfunction",
"w",
encoding="utf-8",
) as f:
f.writelines(
"\n".join(
[
single_cmd.cmd
for single_cmd in midi_cvt.form_java_progress_bar(
maxscore, scoreboard_name, progressbar_style
)
]
)
)
index_file.close()
if os.path.exists(f"{dist_path}/{midi_cvt.music_name}.zip"):
os.remove(f"{dist_path}/{midi_cvt.music_name}.zip")
compress_zipfile(
f"{dist_path}/temp/",
f"{dist_path}/{midi_cvt.music_name}[JEscore].zip",
)
shutil.rmtree(f"{dist_path}/temp/")
return maxlen, maxscore
print(
to_zip_pack_in_score(
Musicreater.experiment.FutureMidiConvertJavaE.from_midi_file(
input("midi路径"),
play_speed=float(input("播放速度:")),
old_exe_format=True,
),
input("输出路径:"),
Musicreater.experiment.ProgressBarStyle(),
# Musicreater.plugin.ConvertConfig(input("输出路径:"),),
scoreboard_name=input("计分板名称:"),
sound_source=input("发音源:"),
auto_reset=True,
)
)

View File

@ -1 +1,2 @@
mido>=1.3
xxhash>=3