Midi歌词支持,文档格式更新

This commit is contained in:
2025-08-19 04:37:11 +08:00
parent 8df63df26c
commit 774f78359c
12 changed files with 698 additions and 195 deletions

1
.gitignore vendored
View File

@ -26,6 +26,7 @@ RES.txt
/Packer/checksum.txt
/bgArrayLib
/fcwslib
test_lyric-mido.py
# Byte-compiled / optimized
__pycache__/

View File

@ -22,8 +22,8 @@ The Licensor of Musicreater("this project") is Eilles, bgArray.
# 若需转载或借鉴 许可声明请查看仓库目录下的 License.md
__version__ = "2.4.0.1"
__vername__ = "全景声支持、音量调节修复"
__version__ = "2.4.1"
__vername__ = "Midi 歌词支持,文档格式更新"
__author__ = (
("金羿", "Eilles"),
("诸葛亮与八卦阵", "bgArray"),
@ -41,13 +41,26 @@ __all__ = [
"SingleNoteBox",
"ProgressBarStyle",
# "TimeStamp", 未来功能
# 默认值
# 字典键
"MIDI_PROGRAM",
"MIDI_VOLUME",
"MIDI_PAN",
# 默认值
"MIDI_DEFAULT_PROGRAM_VALUE",
"MIDI_DEFAULT_VOLUME_VALUE",
"DEFAULT_PROGRESSBAR_STYLE",
# Midi 自己的对照表
"MIDI_PITCH_NAME_TABLE",
"MIDI_PITCHED_NOTE_NAME_GROUP",
"MIDI_PITCHED_NOTE_NAME_TABLE",
"MIDI_PERCUSSION_NOTE_NAME_TABLE",
# Minecraft 自己的对照表
"MC_PERCUSSION_INSTRUMENT_LIST",
"MC_PITCHED_INSTRUMENT_LIST",
"MC_INSTRUMENT_BLOCKS_TABLE",
"MC_EILLES_RTJE12_INSTRUMENT_REPLACE_TABLE",
"MC_EILLES_RTBETA_INSTRUMENT_REPLACE_TABLE",
# Midi 与 游戏 的对照表
"MM_INSTRUMENT_RANGE_TABLE",
"MM_CLASSIC_PITCHED_INSTRUMENT_TABLE",
"MM_CLASSIC_PERCUSSION_INSTRUMENT_TABLE",
@ -62,10 +75,68 @@ __all__ = [
"velocity_2_distance_straight",
"panning_2_rotation_linear",
"panning_2_rotation_trigonometric",
# 工具函数
"load_decode_musicsequence_metainfo",
"load_decode_msq_flush_release",
"load_decode_fsq_flush_release",
"guess_deviation",
"mctick2timestr",
"midi_inst_to_mc_sound",
]
from .main import *
from .main import MusicSequence, MidiConvert
from .subclass import (
MineNote,
MineCommand,
SingleNoteBox,
ProgressBarStyle,
mctick2timestr,
DEFAULT_PROGRESSBAR_STYLE,
)
from .utils import (
# 兼容性函数
load_decode_musicsequence_metainfo,
load_decode_msq_flush_release,
load_decode_fsq_flush_release,
# 工具函数
guess_deviation,
midi_inst_to_mc_sound,
# 处理用函数
velocity_2_distance_natural,
velocity_2_distance_straight,
panning_2_rotation_linear,
panning_2_rotation_trigonometric,
)
from .constants import (
# 字典键
MIDI_PROGRAM,
MIDI_PAN,
MIDI_VOLUME,
# 默认值
MIDI_DEFAULT_PROGRAM_VALUE,
MIDI_DEFAULT_VOLUME_VALUE,
# MIDI 表
MIDI_PITCH_NAME_TABLE,
MIDI_PITCHED_NOTE_NAME_GROUP,
MIDI_PITCHED_NOTE_NAME_TABLE,
MIDI_PERCUSSION_NOTE_NAME_TABLE,
# 我的世界 表
MC_EILLES_RTBETA_INSTRUMENT_REPLACE_TABLE,
MC_EILLES_RTJE12_INSTRUMENT_REPLACE_TABLE,
MC_INSTRUMENT_BLOCKS_TABLE,
MC_PERCUSSION_INSTRUMENT_LIST,
MC_PITCHED_INSTRUMENT_LIST,
# MIDI 到 我的世界 表
MM_INSTRUMENT_RANGE_TABLE,
MM_CLASSIC_PITCHED_INSTRUMENT_TABLE,
MM_CLASSIC_PERCUSSION_INSTRUMENT_TABLE,
MM_TOUCH_PITCHED_INSTRUMENT_TABLE,
MM_TOUCH_PERCUSSION_INSTRUMENT_TABLE,
MM_DISLINK_PITCHED_INSTRUMENT_TABLE,
MM_DISLINK_PERCUSSION_INSTRUMENT_TABLE,
MM_NBS_PITCHED_INSTRUMENT_TABLE,
MM_NBS_PERCUSSION_INSTRUMENT_TABLE,
)

View File

@ -113,13 +113,20 @@ class NoteOnOffMismatchError(MidiFormatException):
"""音符开音和停止不匹配的错误"""
super().__init__("音符不匹配", *args)
class LyricMismatchError(MSCTBaseException):
"""歌词匹配解析错误"""
def __init__(self, *args):
"""有可能产生了错误的歌词解析"""
super().__init__("歌词解析错误", *args)
class ZeroSpeedError(MSCTBaseException, ZeroDivisionError):
"""以0作为播放速度的错误"""
def __init__(self, *args):
"""以0作为播放速度的错误"""
super().__init__("播放速度为0", *args)
super().__init__("播放速度为", *args)
class IllegalMinimumVolumeError(MSCTBaseException, ValueError):

View File

@ -34,6 +34,99 @@ from .types import ChannelType, FittingFunctionType
from .utils import *
class FutureMidiConvertLyricSupport(MidiConvert):
"""
歌词测试支持
"""
def to_command_list_in_delay(
self,
player_selector: str = "@a",
using_lyric: bool = True,
) -> Tuple[List[MineCommand], int, int]:
"""
将midi转换为我的世界命令列表并输出每个音符之后的延迟
Parameters
----------
player_selector: str
玩家选择器,默认为`@a`
Returns
-------
tuple( list[MineCommand指令,...], int音乐时长游戏刻, int最大同时播放的指令数量 )
"""
notes_list: List[MineNote] = sorted(
[i for j in self.channels.values() for i in j],
key=lambda note: note.start_tick,
)
# 此处 我们把通道视为音轨
self.music_command_list = []
multi = max_multi = 0
delaytime_previous = 0
for note in notes_list:
if (tickdelay := (note.start_tick - delaytime_previous)) == 0:
multi += 1
else:
max_multi = max(max_multi, multi)
multi = 0
(
mc_sound_ID,
relative_coordinates,
volume_percentage,
mc_pitch,
) = minenote_to_command_paramaters(
note,
pitch_deviation=self.music_deviation,
)
self.music_command_list.append(
MineCommand(
command=(
self.execute_cmd_head.format(player_selector)
+ r"playsound {} @s ^{} ^{} ^{} {} {} {}".format(
mc_sound_ID,
*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),
)
),
tick_delay=tickdelay,
),
)
if using_lyric and note.extra_info["LYRIC_TEXT"]:
self.music_command_list.append(
MineCommand(
self.execute_cmd_head.format(player_selector)
+ 'title @s title " "'
)
)
self.music_command_list.append(
MineCommand(
self.execute_cmd_head.format(player_selector)
+ "title @s subtitle {}".format(note.extra_info["LYRIC_TEXT"])
)
)
delaytime_previous = note.start_tick
return self.music_command_list, notes_list[-1].start_tick, max_multi + 1
class FutureMidiConvertKamiRES(MidiConvert):
"""
神羽资源包之测试支持
@ -758,10 +851,21 @@ class FutureMidiConvertM4(MidiConvert):
_note: MineNote,
_apply_time_division: float = 10,
) -> List[MineNote]:
"""传入音符数据,返回分割后的插值列表
:param _note: MineNote 音符
:param _apply_time_division: int 间隔帧数
:return list[tuple(int开始时间毫秒, int乐器, int音符, int力度内置, float音量播放),]"""
"""
传入音符数据,返回分割后的插值列表
Parameters
------------
_note: MineNote
音符
_apply_time_division: int
间隔帧数
Returns
---------
list[tuple[int, int, int, int, float]]
分割后的插值列表,每个元素为 (开始时间(毫秒), 乐器, 音符, 力度(内置), 音量(播放))
"""
if _note.percussive:
return [

View File

@ -130,9 +130,7 @@ class MusicSequence:
if minimum_volume_of_music > 1 or minimum_volume_of_music <= 0:
raise IllegalMinimumVolumeError(
"自订的最小音量参数错误:{},应在 (0,1] 范围内。".format(
minimum_volume_of_music
)
"最小音量不得为 {},应在 (0,1] 范围内。".format(minimum_volume_of_music)
)
# max_volume = 1 if max_volume > 1 else (0.001 if max_volume <= 0 else max_volume)
@ -594,7 +592,7 @@ class MusicSequence:
else:
raise MusicSequenceTypeError(
"输入的二进制字节码不是合法的音符序列格式,无法解码,码头前 10 字节为:",
"输入的二进制字节码不是正确的音符序列格式,无法解码,码前十字节为:",
bytes_buffer_in[:10],
)
@ -851,12 +849,12 @@ class MusicSequence:
Returns
-------
以频道作为分割的Midi音符列表字典, 音符总数, 乐器使用统计:
Tuple[MineNoteChannelType, int, Dict[str, int]]
以通道作为分割的Midi音符列表字典, 音符总数, 乐器使用统计
"""
if speed == 0:
raise ZeroSpeedError("播放速度为 0 ,其需要(0,1]范围内的实数。")
raise ZeroSpeedError("播放速度不得为零,应为 (0,1] 范围内的实数。")
# 一个midi中仅有16个通道 我们通过通道来识别而不是音轨
midi_channels: MineNoteChannelType = empty_midi_channels(default_staff=[])
@ -893,8 +891,15 @@ class MusicSequence:
],
] = empty_midi_channels(default_staff=[])
lyric_cache: List[Tuple[int, str]] = []
# 直接使用mido.midifiles.tracks.merge_tracks转为单轨
# 采用的遍历信息思路
# 采用的遍历信息思路
# 来自 202508 的留言
# 该处代码有点问题
# merged track 丢失了 track 信息,会导致音符不匹配的问题出现
# 应该用遍历 Track 的方式来处理
for msg in midi.merged_track:
if msg.time != 0:
# 微秒
@ -904,36 +909,66 @@ class MusicSequence:
if msg.type == "set_tempo":
tempo = msg.tempo
elif msg.type == "program_change":
# 检测 乐器变化 之 midi 事件
channel_controler[msg.channel][MIDI_PROGRAM] = msg.program
elif msg.is_cc(7):
# Control Change 更改当前通道的 音量 的事件(大幅度)
channel_controler[msg.channel][MIDI_VOLUME] = msg.value
elif msg.is_cc(10):
# Control Change 更改当前通道的 音调偏移 的事件(大幅度)
channel_controler[msg.channel][MIDI_PAN] = msg.value
elif msg.type == "lyrics":
# 歌词事件
lyric_cache.append((microseconds, msg.text))
# print(lyric_cache, flush=True)
elif msg.type == "note_on" and msg.velocity != 0:
# 一个音符开始弹奏
# 加入音符队列甲(按通道分隔)
# (音高,乐器)
note_queue_A[msg.channel].append(
(msg.note, channel_controler[msg.channel][MIDI_PROGRAM])
)
# 音符队列乙(按通道分隔)
# (力度,微秒)
note_queue_B[msg.channel].append((msg.velocity, microseconds))
elif (msg.type == "note_off") or (
msg.type == "note_on" and msg.velocity == 0
):
# 一个音符结束弹奏
if (
msg.note,
channel_controler[msg.channel][MIDI_PROGRAM],
) in note_queue_A[msg.channel]:
# 在甲队列中发现了同一个 音高和乐器 的音符
# 获取其音符力度和微秒数
_velocity, _ms = note_queue_B[msg.channel][
note_queue_A[msg.channel].index(
(msg.note, channel_controler[msg.channel][MIDI_PROGRAM])
)
]
# 在队列中删除此音符
note_queue_A[msg.channel].remove(
(msg.note, channel_controler[msg.channel][MIDI_PROGRAM])
)
note_queue_B[msg.channel].remove((_velocity, _ms))
_lyric = ""
# 找一找歌词吧
if lyric_cache:
for i in range(len(lyric_cache)):
if lyric_cache[i][0] >= _ms:
_lyric = lyric_cache.pop(i)[1]
break
# 更新结果信息
midi_channels[msg.channel].append(
that_note := midi_msgs_to_minenote(
inst_=(
@ -961,14 +996,19 @@ class MusicSequence:
volume_processing_method_=vol_processing_function,
panning_processing_method_=pan_processing_function,
note_table_replacement=note_rtable_replacement,
lyric_line=_lyric,
)
)
# 更新统计信息
note_count += 1
if that_note.sound_name in note_count_per_instrument.keys():
note_count_per_instrument[that_note.sound_name] += 1
else:
note_count_per_instrument[that_note.sound_name] = 1
else:
# 什么?找不到 note on 消息??
if ignore_mismatch_error:
print(
"[WARRING] MIDI格式错误 音符不匹配 {} 无法在上文中找到与之匹配的音符开音消息".format(
@ -993,7 +1033,24 @@ class MusicSequence:
3 音符结束消息
("NoteE", 结束的音符ID, 距离演奏开始的毫秒)"""
del tempo
if lyric_cache:
# 怎么有歌词多啊
if ignore_mismatch_error:
print(
"[WARRING] MIDI 解析错误 歌词对应错误,以下歌词未能填入音符之中,已经填入的仍可能有误 {}".format(
lyric_cache
)
)
else:
raise LyricMismatchError(
"MIDI 解析产生错误",
"歌词解析过程中无法对应音符,已填入的音符仍可能有误",
lyric_cache,
)
channels = dict(
[
(channel_no, sorted(channel_notes, key=lambda note: note.start_tick))
@ -1126,6 +1183,7 @@ class MidiConvert(MusicSequence):
pan_processing_func: FittingFunctionType = panning_2_rotation_linear,
music_pitch_deviation: float = 0,
note_table_replacement: Dict[str, str] = {},
midi_charset: str = "utf-8",
):
"""
直接输入文件地址,将 midi 文件读入
@ -1171,6 +1229,7 @@ class MidiConvert(MusicSequence):
return cls.from_mido_obj(
midi_obj=mido.MidiFile(
midi_file_path,
charset=midi_charset,
clip=True,
),
midi_name=midi_music_name,

View File

@ -25,12 +25,28 @@ from typing import List, Literal, Union
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
"""
使用指定的压缩算法将目录打包为zip文件
Parameters
------------
sourceDir: str
要压缩的源目录路径
outFilename: str
输出的zip文件路径
compression: int, 可选
压缩算法默认为8 (DEFLATED)
可用算法:
STORED = 0
DEFLATED = 8 (默认)
BZIP2 = 12
LZMA = 14
exceptFile: list[str], 可选
需要排除在压缩包外的文件名称列表(可选)
Returns
---------
None
"""
zipf = zipfile.ZipFile(outFilename, "w", compression)

View File

@ -60,50 +60,55 @@ def form_command_block_in_BDX_bytes(
customName: str = "",
executeOnFirstTick: bool = False,
trackOutput: bool = True,
):
) -> bytes:
"""
使用指定项目返回指定的指令方块放置指令项
:param command: `str`
使用指定参数生成指定的指令方块放置指令项
Parameters
------------
command: str
指令
:param particularValue:
particularValue: int
方块特殊值,即朝向
:0 下 无条件
:1 上 无条件
:2 z轴负方向 无条件
:3 z轴正方向 无条件
:4 x轴负方向 无条件
:5 x轴正方向 无条件
:6 下 无条件
:7 下 无条件
:8 下 有条件
:9 上 有条件
:10 z轴方向 有条件
:11 z轴正方向 有条件
:12 x轴方向 有条件
:13 x轴正方向 有条件
:14 下 有条件
:14 下 有条件
: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`
impluse: int (0|1|2)
方块类型
0脉冲 1循环 2连锁
:param condition: `bool`
0脉冲 1循环 2连锁
condition: bool
是否有条件
:param needRedstone: `bool`
needRedstone: bool
是否需要红石
:param tickDelay: `int`
tickDelay: int
执行延时
:param customName: `str`
customName: str
悬浮字
lastOutput: `str`
上次输出字符串,注意此处需要留空
:param executeOnFirstTick: `bool`
首刻执行(循环指令方块是否激活后立即执行若为False则从激活时起延迟后第一次执行)
:param trackOutput: `bool`
是否输出
lastOutput: str
命令方块的上次输出字符串,注意此处需要留空
executeOnFirstTick: bool
是否启用首刻执行循环指令方块是否激活后立即执行若为False则从激活时起延迟后第一次执行
trackOutput: bool
是否启用命令方块输出
:return:str
Returns
---------
bytes
用以生成 bdx 结构的字节码
"""
block = b"\x24" + particularValue.to_bytes(2, byteorder="big", signed=False)
@ -127,9 +132,19 @@ def commands_to_BDX_bytes(
max_height: int = 64,
):
"""
:param commands: 指令列表(指令, 延迟)
:param max_height: 生成结构最大高度
:return 成功与否,成功返回(True,未经过压缩的源,结构占用大小),失败返回(False,str失败原因)
指令列表转换为用以生成 bdx 结构的字节码
Parameters
------------
commands: list[tuple[str, int]]
指令列表,每个元素为 (指令, 延迟)
max_height: int
生成结构最大高度
Returns
---------
tuple[bool, bytes, int] or tuple[bool, str]
成功与否,成功返回 (True, 未经过压缩的源, 结构占用大小),失败返回 (False, str失败原因)
"""
_sideLength = bottem_side_length_of_smallest_square_bottom_box(

View File

@ -19,9 +19,22 @@ Terms & Conditions: License.md in the root directory
import math
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 bottem_side_length_of_smallest_square_bottom_box(
_total_block_count: int, _max_height: int
):
"""
给定结构的总方块数量和规定的最大高度,返回该结构应当构成的图形,在底面的外切正方形边长
Parameters
------------
_total_block_count: int
总方块数量
_max_height: int
规定的结构最大高度
Returns
---------
int
外切正方形的边长
"""
return math.ceil(math.sqrt(math.ceil(_total_block_count / _max_height)))

View File

@ -70,17 +70,25 @@ def form_note_block_in_NBT_struct(
instrument: str = "note.harp",
powered: bool = False,
compability_version_number: int = COMPABILITY_VERSION_119,
):
"""生成音符盒方块
:param note: `int`(0~24)
) -> Block:
"""
生成音符盒方块
Parameters
------------
note: int (0~24)
音符的音高
:param coordinate: `tuple[int,int,int]`
coordinate: tuple[int, int, int]
此方块所在之相对坐标
:param instrument: `str`
instrument: str
音符盒的乐器
:param powered: `bool`
powered: bool
是否已被激活
:return Block
Returns
-------
Block
生成的方块对象
"""
return Block(
@ -108,15 +116,26 @@ def form_repeater_in_NBT_struct(
delay: int,
facing: int,
compability_version_number: int = COMPABILITY_VERSION_119,
):
"""生成中继器方块
:param facing: 朝向:
) -> Block:
"""
生成中继器方块
Parameters
----------
facing: int (0~3)
朝向:
Z- 北 0
X- 东 1
Z+ 南 2
X+ 西 3
:param delay: 0~3
:return Block()"""
delay: int (0~3)
信号延迟
Returns
-------
Block
生成的方块对象
"""
return Block(
"minecraft",
@ -141,50 +160,58 @@ def form_command_block_in_NBT_struct(
executeOnFirstTick: bool = False,
trackOutput: bool = True,
compability_version_number: int = COMPABILITY_VERSION_119,
):
) -> Block:
"""
使用指定项目返回指定的指令方块结构
:param command: `str`
使用指定参数生成指令方块
Parameters
----------
command: str
指令
:param coordinate: `tuple[int,int,int]`
coordinate: tuple[int,int,int]
此方块所在之相对坐标
:param particularValue:
particularValue: int
方块特殊值,即朝向
:0 下 无条件
:1 上 无条件
:2 z轴负方向 无条件
:3 z轴正方向 无条件
:4 x轴负方向 无条件
:5 x轴正方向 无条件
:6 下 无条件
:7 下 无条件
:8 下 有条件
:9 上 有条件
:10 z轴方向 有条件
:11 z轴正方向 有条件
:12 x轴方向 有条件
:13 x轴正方向 有条件
:14 下 有条件
:14 下 有条件
: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`
impluse: int (0|1|2)
方块类型
0脉冲 1循环 2连锁
:param condition: `bool`
0脉冲 1循环 2连锁
condition: bool
是否有条件
:param alwaysRun: `bool`
alwaysRun: bool
是否始终执行
:param tickDelay: `int`
tickDelay: int
执行延时
:param customName: `str`
customName: str
悬浮字
:param executeOnFirstTick: `bool`
首刻执行(循环指令方块是否激活后立即执行若为False则从激活时起延迟后第一次执行)
:param trackOutput: `bool`
是否输出
executeOnFirstTick: bool
是否启用首刻执行循环指令方块是否激活后立即执行若为False则从激活时起延迟后第一次执行
trackOutput: bool
是否启用命令方块输出
compability_version_number: int
版本兼容代号
:return:str
Returns
-------
Block
生成的方块对象
"""
return Block(
@ -231,9 +258,19 @@ def commands_to_structure(
compability_version_: int = COMPABILITY_VERSION_119,
):
"""
:param commands: 指令列表
:param max_height: 生成结构最大高度
:return 结构类,结构占用大小,终点坐标
由指令列表生成(纯指令方块)结构
Parameters
------------
commands: list
指令列表
max_height: int
生成结构最大高度
Returns
---------
Structure, tuple[int, int, int], tuple[int, int, int]
结构类, 结构占用大小, 终点坐标
"""
_sideLength = bottem_side_length_of_smallest_square_bottom_box(
@ -321,12 +358,25 @@ def commands_to_redstone_delay_structure(
compability_version_: int = COMPABILITY_VERSION_119,
) -> Tuple[Structure, Tuple[int, int, int], Tuple[int, int, int]]:
"""
:param commands: 指令列表
:param delay_length: 延时总长
:param max_multicmd_length: 最大同时播放的音符数量
:param base_block: 生成结构的基底方块
:param axis_: 生成结构的延展方向
:return 结构类,结构占用大小,终点坐标
由指令列表生成由红石中继器延迟的结构
Parameters
------------
commands: list
指令列表
delay_length: int
延时总长
max_multicmd_length: int
最大同时播放的音符数量
base_block: Block
生成结构的基底方块
axis_: str
生成结构的延展方向
Returns
---------
Structure, tuple[int, int, int], tuple[int, int, int]
结构类, 结构占用大小, 终点坐标
"""
if axis_ in ["z+", "Z+"]:
extensioon_direction = z

View File

@ -70,23 +70,41 @@ class MineNote:
azimuth: Optional[Tuple[float, float]] = None,
extra_information: Optional[Dict[str, Any]] = None,
):
"""用于存储单个音符的类
"""
用于存储单个音符的类
:param mc_sound_name:`str` 《我的世界》声音ID
:param midi_pitch:`int` midi音高
:param midi_velocity:`int` midi响度(力度)
:param start_time:`int` 开始之时(命令刻)
Parameters
------------
mc_sound_name: str
《我的世界》声音ID
midi_pitch: int
midi音高
midi_velocity: int
midi响度(力度)
start_time: int
开始之时(命令刻)
注:此处的时间是用从乐曲开始到当前的刻数
:param last_time:`int` 音符延续时间(命令刻)
:param mass_precision_time:`int` 高精度的开始时间偏移量(1/1250秒)
:param is_percussion:`bool` 是否作为打击乐器
:param distance: `float` 发声源距离玩家的距离(半径 `r`
last_time: int
音符延续时间(命令刻)
mass_precision_time: int
高精度的开始时间偏移量(1/1250秒)
is_percussion: bool
是否作为打击乐器
distance: float
发声源距离玩家的距离(半径 `r`
注:距离越近,音量越高,默认为 0。此参数可以与音量成某种函数关系。
:param azimuth:`tuple[float, float]` 声源方位
azimuth: tuple[float, float]
声源方位
此参数为tuple包含两个元素分别表示
`rV` 发声源在竖直(上下)轴上,从玩家视角正前方开始,向顺时针旋转的角度
`rH` 发声源在水平(左右)轴上,从玩家视角正前方开始,向上(到达玩家正上方顶点后变为向下,以此类推的旋转)旋转的角度
:param extra_information:`Any` 附加信息"""
extra_information: Any
附加信息,尽量存储为字典
Returns
---------
MineNote 类
"""
self.sound_name: str = mc_sound_name
"""乐器ID"""
self.note_pitch: int = 66 if midi_pitch is None else midi_pitch
@ -94,9 +112,9 @@ class MineNote:
self.velocity: int = midi_velocity
"""响度(力度)"""
self.start_tick: int = start_time
"""开始之时 tick"""
"""开始之时 命令刻"""
self.duration: int = last_time
"""音符持续时间 tick"""
"""音符持续时间 命令刻"""
self.high_precision_time: int = mass_precision_time
"""高精度开始时间偏量 0.4 毫秒"""
@ -133,17 +151,35 @@ class MineNote:
displacement: Optional[Tuple[float, float, float]] = None,
extra_information: Optional[Any] = None,
):
"""用于存储单个音符的类
:param mc_sound_name:`str` 《我的世界》声音ID
:param midi_pitch:`int` midi音高
:param midi_velocity:`int` midi响度(力度)
:param start_time:`int` 开始之时(命令刻)
"""
从传统音像位移格式传参,写入用于存储单个音符的类
Parameters
------------
mc_sound_name: str
《我的世界》声音ID
midi_pitch: int
midi音高
midi_velocity: int
midi响度(力度)
start_time: int
开始之时(命令刻)
注:此处的时间是用从乐曲开始到当前的刻数
:param last_time:`int` 音符延续时间(命令刻)
:param mass_precision_time:`int` 高精度的开始时间偏移量(1/1250秒)
:param is_percussion:`bool` 是否作为打击乐器
:param displacement:`tuple[float,float,float]` 声像位移
:param extra_information:`Any` 附加信息"""
last_time: int
音符延续时间(命令刻)
mass_precision_time: int
高精度的开始时间偏移量(1/1250秒)
is_percussion: bool
是否作为打击乐器
displacement: tuple[float, float, float]
声像位移
extra_information: Any
附加信息,尽量为字典。
Returns
---------
MineNote 类
"""
if displacement is None:
displacement = (0, 0, 0)
@ -246,10 +282,17 @@ class MineNote:
"""
将数据打包为字节码
:param is_displacement_included:`bool` 是否包含声像偏移数据,默认为**是**
:param is_high_time_precision:`bool` 是否启用高精度,默认为**是**
Parameters
------------
is_displacement_included: bool
是否包含声像偏移数据,默认为**是**
is_high_time_precision: bool
是否启用高精度,默认为**是**
:return bytes 打包好的字节码
Returns
---------
bytes
打包好的字节码
"""
# MineNote 的字节码共有三个顺次版本分别如下
@ -545,12 +588,26 @@ class SingleNoteBox:
percussion: Optional[bool] = None,
annotation: str = "",
):
"""用于存储单个音符盒的类
:param instrument_block_ 音符盒演奏所使用的乐器方块
:param note_value_ 音符盒的演奏音高
:param percussion 此音符盒乐器是否作为打击乐处理
"""
用于存储单个音符盒的类
Parameters
------------
instrument_block_: str
音符盒演奏所使用的乐器方块
note_value_: int
音符盒的演奏音高
percussion: bool
此音符盒乐器是否作为打击乐处理
注:若为空,则自动识别是否为打击乐器
:param annotation 音符注释"""
annotation: Any
音符注释
Returns
---------
SingleNoteBox 类
"""
self.instrument_block = instrument_block_
"""乐器方块"""
self.note_value = note_value_
@ -634,7 +691,8 @@ class ProgressBarStyle:
to_play_s: Optional[str] = None,
played_s: Optional[str] = None,
):
"""用于存储进度条样式的类
"""
用于存储进度条样式的类
| 标识符 | 指定的可变量 |
|---------|----------------|
@ -646,9 +704,18 @@ class ProgressBarStyle:
| `%%%` | 当前进度比率 |
| `_` | 用以表示进度条占位|
:param base_s 基础样式,用以定义进度条整体
:param to_play_s 进度条样式:尚未播放的样子
:param played_s 已经播放的样子
Parameters
------------
base_s: str
基础样式,用以定义进度条整体
to_play_s: str
进度条样式:尚未播放的样子
played_s: str
已经播放的样子
Returns
---------
ProgressBarStyle 类
"""
self.base_style = (
@ -709,9 +776,19 @@ class ProgressBarStyle:
"""
直接依照此格式输出一个进度条
:param played_delays: int 当前播放进度积分值
:param total_delays: int 乐器总延迟数(积分数)
:param music_name: str 曲名
Parameters
------------
played_delays: int
当前播放进度积分值
total_delays: int
乐器总延迟数(计分板值)
music_name: str
曲名
Returns
---------
str
进度条字符串
"""
return (

View File

@ -136,7 +136,7 @@ def velocity_2_distance_natural(
Parameters
----------
vol: int
midi音符力度值
midi 音符力度值
Returns
-------
@ -162,7 +162,7 @@ def velocity_2_distance_straight(vol: float) -> float:
Parameters
----------
vol: int
midi音符力度值
midi 音符力度值
Returns
-------
@ -179,7 +179,7 @@ def panning_2_rotation_linear(pan_: float) -> float:
----------
pan_: int
Midi 左右平衡偏移值
此参数为int范围从0到127当为 64 时,声源居中
此参数为int范围从 0 到 127当为 64 时,声源居中
Returns
-------
@ -197,7 +197,7 @@ def panning_2_rotation_trigonometric(pan_: float) -> float:
----------
pan_: int
Midi 左右平衡偏移值
此参数为int范围从0到127当为 64 时,声源居中
此参数为int范围从 0 到 127当为 64 时,声源居中
Returns
-------
@ -222,11 +222,19 @@ def minenote_to_command_paramaters(
Union[float, Literal[None]],
]:
"""
将MineNote对象转为《我的世界》音符播放所需之参数
:param note_:MineNote 音符对象
:param deviation:float 音调偏移量
MineNote 对象转为《我的世界》音符播放所需之参数
:return str[我的世界音符ID], Tuple[float,float,float]播放视角坐标, float[指令音量参数], float[指令音调参数]
Parameters
------------
note_: MineNote
音符对象
deviation: float
音调偏移量
Returns
---------
str, tuple[float, float, float], float, float
我的世界音符ID, 播放视角坐标, 指令音量参数, 指令音调参数
"""
return (
@ -252,7 +260,6 @@ def minenote_to_command_paramaters(
)
def midi_msgs_to_minenote(
inst_: int, # 乐器编号
note_: int,
@ -267,24 +274,46 @@ def midi_msgs_to_minenote(
volume_processing_method_: FittingFunctionType,
panning_processing_method_: FittingFunctionType,
note_table_replacement: Dict[str, str] = {},
lyric_line: str = "",
) -> MineNote:
"""
将Midi信息转为我的世界音符对象
:param inst_: int 乐器编号
:param note_: int 音高编号(音符编号)
:param percussive_: bool 是否作为打击乐器启用
:param volume_: int 音量
:param velocity_: int 力度
:param panning_: int 声相偏移
:param start_time_: int 音符起始时间(微秒)
:param duration_: int 音符持续时间(微秒)
:param play_speed: float 曲目播放速度
:param midi_reference_table: Dict[int, str] 转换对照表
:param volume_processing_method_: Callable[[float], float] 音量处理函数
:param panning_processing_method_: Callable[[float], float] 立体声相偏移处理函数
:param note_table_replacement: Dict[str, str] 音符替换表,定义 Minecraft 音符字串的替换
:return MineNote我的世界音符对象
Parameters
------------
inst_: int
乐器编号
note_: int
音高编号(音符编号)
percussive_: bool
是否作为打击乐器启用
volume_: int
音量
velocity_: int
力度
panning_: int
声相偏移
start_time_: int
音符起始时间(微秒)
duration_: int
音符持续时间(微秒)
play_speed: float
曲目播放速度
midi_reference_table: Dict[int, str]
转换对照表
volume_processing_method_: Callable[[float], float]
音量处理函数
panning_processing_method_: Callable[[float], float]
立体声相偏移处理函数
note_table_replacement: Dict[str, str]
音符替换表,定义 Minecraft 音符字串的替换
lyric_line: str
该音符的歌词
Returns
---------
MineNote
我的世界音符对象
"""
mc_sound_ID = midi_inst_to_mc_sound(
inst_,
@ -302,6 +331,11 @@ def midi_msgs_to_minenote(
is_percussion=percussive_,
distance=volume_processing_method_(volume_),
azimuth=(panning_processing_method_(panning_), 0),
extra_information={
"LYRIC_TEXT": lyric_line,
"VOLUME_VALUE": volume_,
"PIN_VALUE": panning_,
},
)
@ -319,24 +353,46 @@ def midi_msgs_to_minenote_using_kami_respack(
volume_processing_method_: Callable[[float], float],
panning_processing_method_: FittingFunctionType,
note_table_replacement: Dict[str, str] = {},
lyric_line: str = "",
) -> MineNote:
"""
将Midi信息转为我的世界音符对象
:param inst_: int 乐器编号
:param note_: int 音高编号(音符编号)
:param percussive_: bool 是否作为打击乐器启用
:param volume_: int 音量
:param velocity_: int 力度
:param panning_: int 声相偏移
:param start_time_: int 音符起始时间(微秒)
:param duration_: int 音符持续时间(微秒)
:param play_speed: float 曲目播放速度
:param midi_reference_table: Dict[int, str] 转换对照表
:param volume_processing_method_: Callable[[float], float] 音量处理函数
:param panning_processing_method_: Callable[[float], float] 立体声相偏移处理函数
:param note_table_replacement: Dict[str, str] 音符替换表,定义 Minecraft 音符字串的替换
将Midi信息转为我的世界音符对象,使用神羽资源包兼容格式
:return MineNote我的世界音符对象
Parameters
------------
inst_: int
乐器编号
note_: int
音高编号(音符编号)
percussive_: bool
是否作为打击乐器启用
volume_: int
音量
velocity_: int
力度
panning_: int
声相偏移
start_time_: int
音符起始时间(微秒)
duration_: int
音符持续时间(微秒)
play_speed: float
曲目播放速度
midi_reference_table: Dict[int, str]
转换对照表
volume_processing_method_: Callable[[float], float]
音量处理函数
panning_processing_method_: Callable[[float], float]
立体声相偏移处理函数
note_table_replacement: Dict[str, str]
音符替换表,定义 Minecraft 音符字串的替换
lyric_line: str
该音符的歌词
Returns
---------
MineNote
我的世界音符对象
"""
using_original = False
@ -371,6 +427,7 @@ def midi_msgs_to_minenote_using_kami_respack(
"USING_ORIGINAL_SOUND": using_original, # 判断 extra_information 中是否有 USING_ORIGINAL_SOUND 键是判断是否使用神羽资源包解析的一个显著方法
"INST_VALUE": note_ if percussive_ else inst_,
"NOTE_VALUE": inst_ if percussive_ else note_,
"LYRIC_TEXT": lyric_line,
"VOLUME_VALUE": volume_,
"PIN_VALUE": panning_,
},

33
test_future_lyric.py Normal file
View File

@ -0,0 +1,33 @@
import Musicreater.experiment
import Musicreater.plugin
import Musicreater.plugin.mcstructfile
msct = Musicreater.experiment.FutureMidiConvertLyricSupport.from_midi_file(
input("midi路径:"), old_exe_format=False
)
opt = input("输出路径:")
# print(
# "乐器使用情况",
# )
# for name in sorted(
# set(
# [
# n.split(".")[0].replace("c", "").replace("d", "")
# for n in msct.note_count_per_instrument.keys()
# ]
# )
# ):
# print("\t", name, flush=True)
print(
"\n输出:",
Musicreater.plugin.mcstructfile.to_mcstructure_file_in_delay(
msct,
opt,
# Musicreater.plugin.ConvertConfig(input("输出路径:"),),
max_height=32,
),
)