This commit is contained in:
2025-07-06 02:59:14 +08:00
11 changed files with 654 additions and 251 deletions

View File

@ -22,8 +22,8 @@ The Licensor of Musicreater("this project") is Eilles, bgArray.
# 若需转载或借鉴 许可声明请查看仓库目录下的 License.md
__version__ = "2.3.2"
__vername__ = "支持神羽资源包"
__version__ = "2.4.0"
__vername__ = "全景声支持、音量调节修复"
__author__ = (
("金羿", "Eilles"),
("诸葛亮与八卦阵", "bgArray"),
@ -42,7 +42,11 @@ __all__ = [
"ProgressBarStyle",
# "TimeStamp", 未来功能
# 默认值
"MIDI_PROGRAM",
"MIDI_VOLUME",
"MIDI_PAN",
"MIDI_DEFAULT_PROGRAM_VALUE",
"MIDI_DEFAULT_VOLUME_VALUE",
"DEFAULT_PROGRESSBAR_STYLE",
"MM_INSTRUMENT_RANGE_TABLE",
"MM_CLASSIC_PITCHED_INSTRUMENT_TABLE",
@ -56,6 +60,8 @@ __all__ = [
# 操作性函数
"natural_curve",
"straight_line",
"panning_2_rotation_linear",
"panning_2_rotation_trigonometric",
"load_decode_musicsequence_metainfo",
"load_decode_msq_flush_release",
"load_decode_fsq_flush_release",

View File

@ -34,9 +34,22 @@ z = "z"
z
"""
MIDI_PROGRAM = "program"
"""Midi乐器编号"""
MIDI_VOLUME = "volume"
"""Midi通道音量"""
MIDI_PAN = "pan"
"""Midi通道立体声场偏移"""
# Midi用对照表
MIDI_DEFAULT_VOLUME_VALUE: int = (
64 # Midi默认音量当用户未指定时默认使用折中默认音量
)
MIDI_DEFAULT_PROGRAM_VALUE: int = (
74 # 当 Midi 本身与用户皆未指定音色时,默认 Flute 长笛
)

View File

@ -27,6 +27,8 @@ from .main import (
MidiConvert,
mido,
)
from .constants import MIDI_PAN, MIDI_PROGRAM, MIDI_VOLUME
from .subclass import *
from .types import ChannelType, FittingFunctionType
from .utils import *
@ -36,16 +38,19 @@ class FutureMidiConvertKamiRES(MidiConvert):
"""
神羽资源包之测试支持
"""
@staticmethod
def to_music_note_channels(
midi: mido.MidiFile,
ignore_mismatch_error: bool = True,
speed: float = 1.0,
default_program_value: int = -1,
default_volume_value: int = 64,
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,
vol_processing_function: FittingFunctionType = natural_curve,
pan_processing_function: FittingFunctionType = panning_2_rotation_trigonometric,
note_rtable_replacement: Dict[str, str] = {},
) -> Tuple[MineNoteChannelType, int, Dict[str, int]]:
"""
@ -59,6 +64,8 @@ class FutureMidiConvertKamiRES(MidiConvert):
音乐播放速度倍数
default_program_value: int
默认的 MIDI 乐器值
default_volume_value: int
默认的通道音量值
default_tempo_value: int
默认的 MIDI TEMPO 值
pitched_note_rtable: Dict[int, Tuple[str, int]]
@ -66,7 +73,9 @@ class FutureMidiConvertKamiRES(MidiConvert):
percussion_note_rtable: Dict[int, Tuple[str, int]]
打击乐器Midi-MC对照表
vol_processing_function: Callable[[float], float]
声像偏移拟合函数
音量对播放距离的拟合函数
pan_processing_function: Callable[[float], float]
声像偏移对播放旋转角度的拟合函数
note_rtable_replacement: Dict[str, str]
音符名称替换表,此表用于对 Minecraft 乐器名称进行替换,而非 Midi Program 的替换
@ -81,9 +90,15 @@ class FutureMidiConvertKamiRES(MidiConvert):
# 一个midi中仅有16个通道 我们通过通道来识别而不是音轨
midi_channels: MineNoteChannelType = empty_midi_channels(default_staff=[])
channel_program: Dict[int, int] = empty_midi_channels(
default_staff=default_program_value
channel_controler: Dict[int, Dict[str, int]] = empty_midi_channels(
default_staff={
MIDI_PROGRAM: default_program_value,
MIDI_VOLUME: default_volume_value,
MIDI_PAN: 64,
}
)
tempo = default_tempo_value
note_count = 0
note_count_per_instrument: Dict[str, int] = {}
@ -118,76 +133,84 @@ class FutureMidiConvertKamiRES(MidiConvert):
# 简化
if msg.type == "set_tempo":
tempo = msg.tempo
else:
if msg.type == "program_change":
channel_program[msg.channel] = msg.program
elif msg.type == "program_change":
channel_controler[msg.channel][MIDI_PROGRAM] = msg.program
elif msg.type == "note_on" and msg.velocity != 0:
note_queue_A[msg.channel].append(
(msg.note, channel_program[msg.channel])
elif msg.is_cc(7):
channel_controler[msg.channel][MIDI_VOLUME] = msg.value
elif msg.is_cc(10):
channel_controler[msg.channel][MIDI_PAN] = msg.value
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].append((msg.velocity, microseconds))
note_queue_B[msg.channel].remove((_velocity, _ms))
elif (msg.type == "note_off") or (
msg.type == "note_on" and msg.velocity == 0
):
if (msg.note, channel_program[msg.channel]) in note_queue_A[
msg.channel
]:
_velocity, _ms = note_queue_B[msg.channel][
note_queue_A[msg.channel].index(
(msg.note, channel_program[msg.channel])
)
]
note_queue_A[msg.channel].remove(
(msg.note, channel_program[msg.channel])
midi_channels[msg.channel].append(
that_note := midi_msgs_to_minenote_using_kami_respack(
inst_=(
msg.note
if msg.channel == 9
else channel_controler[msg.channel][MIDI_PROGRAM]
),
note_=(
channel_controler[msg.channel][MIDI_PROGRAM]
if msg.channel == 9
else msg.note
),
percussive_=(msg.channel == 9),
volume_=channel_controler[msg.channel][MIDI_VOLUME],
velocity_=_velocity,
panning_=channel_controler[msg.channel][MIDI_PAN],
start_time_=_ms, # 微秒
duration_=microseconds - _ms, # 微秒
play_speed=speed,
midi_reference_table=(
percussion_note_rtable
if msg.channel == 9
else pitched_note_rtable
),
volume_processing_method_=vol_processing_function,
panning_processing_method_=pan_processing_function,
note_table_replacement=note_rtable_replacement,
)
note_queue_B[msg.channel].remove((_velocity, _ms))
midi_channels[msg.channel].append(
that_note := midi_msgs_to_minenote_using_kami_respack(
inst_=(
msg.note
if msg.channel == 9
else channel_program[msg.channel]
),
note_=(
channel_program[msg.channel]
if msg.channel == 9
else msg.note
),
percussive_=(msg.channel == 9),
velocity_=_velocity,
start_time_=_ms, # 微秒
duration_=microseconds - _ms, # 微秒
play_speed=speed,
midi_reference_table=(
percussion_note_rtable
if msg.channel == 9
else pitched_note_rtable
),
volume_processing_method_=vol_processing_function,
note_table_replacement=note_rtable_replacement,
)
)
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
)
note_count += 1
if that_note.sound_name in note_count_per_instrument.keys():
note_count_per_instrument[that_note.sound_name] += 1
else:
if ignore_mismatch_error:
print(
"[WARRING] MIDI格式错误 音符不匹配 {} 无法在上文中找到与之匹配的音符开音消息".format(
msg
)
)
else:
raise NoteOnOffMismatchError(
"当前的MIDI很可能有损坏之嫌……",
msg,
"无法在上文中找到与之匹配的音符开音消息。",
note_count_per_instrument[that_note.sound_name] = 1
else:
if ignore_mismatch_error:
print(
"[WARRING] MIDI格式错误 音符不匹配 {} 无法在上文中找到与之匹配的音符开音消息".format(
msg
)
)
else:
raise NoteOnOffMismatchError(
"当前的MIDI很可能有损坏之嫌……",
msg,
"无法在上文中找到与之匹配的音符开音消息。",
)
"""整合后的音乐通道格式
每个通道包括若干消息元素其中逃不过这三种:
@ -214,7 +237,6 @@ class FutureMidiConvertKamiRES(MidiConvert):
note_count_per_instrument,
)
def to_command_list_in_score(
self,
scoreboard_name: str = "mscplay",
@ -362,7 +384,6 @@ class FutureMidiConvertKamiRES(MidiConvert):
return self.music_command_list, notes_list[-1].start_tick, max_multi + 1
class FutureMidiConvertJavaE(MidiConvert):
def form_java_progress_bar(

View File

@ -164,11 +164,13 @@ class MusicSequence:
mismatch_error_ignorance: bool = True,
speed_multiplier: float = 1,
default_midi_program: int = MIDI_DEFAULT_PROGRAM_VALUE,
default_midi_volume: int = MIDI_DEFAULT_VOLUME_VALUE,
default_tempo: int = mido.midifiles.midifiles.DEFAULT_TEMPO,
pitched_note_referance_table: MidiInstrumentTableType = MM_TOUCH_PITCHED_INSTRUMENT_TABLE,
percussion_note_referance_table: MidiInstrumentTableType = MM_TOUCH_PERCUSSION_INSTRUMENT_TABLE,
minimum_vol: float = 0.1,
volume_processing_function: FittingFunctionType = natural_curve,
panning_processing_function: FittingFunctionType = panning_2_rotation_linear,
deviation: float = 0,
note_referance_table_replacement: Dict[str, str] = {},
):
@ -177,7 +179,7 @@ class MusicSequence:
Paramaters
==========
mido_file: mido.MidiFile 对象
mido_file: mido.MidiFile
需要处理的midi对象
midi_music_name: str
音乐名称
@ -186,7 +188,9 @@ class MusicSequence:
speed_multiplier: float
音乐播放速度倍数
default_midi_program: int
默认的MIDI Program值
默认的 MIDI Program值
default_midi_volume: int
默认的 MIDI 音量
default_tempo: int
默认的MIDI TEMPO值
pitched_note_referance_table: Dict[int, Tuple[str, int]]
@ -196,7 +200,9 @@ class MusicSequence:
minimum_vol: float
播放的最小音量 应为 (0,1] 范围内的小数
volume_processing_function: Callable[[float], float]
声像偏移拟合函数
音量对播放距离的拟合函数
panning_processing_function: Callable[[float], float]
声像偏移对播放旋转角度的拟合函数
deviation: float
全曲音调偏移值
note_referance_table_replacement: Dict[str, str]
@ -210,13 +216,15 @@ class MusicSequence:
inst_note_count,
) = cls.to_music_note_channels(
midi=mido_file,
ignore_mismatch_error=mismatch_error_ignorance,
speed=speed_multiplier,
default_program_value=default_midi_program,
default_volume_value=default_midi_volume,
default_tempo_value=default_tempo,
pitched_note_rtable=pitched_note_referance_table,
percussion_note_rtable=percussion_note_referance_table,
default_program_value=default_midi_program,
default_tempo_value=default_tempo,
vol_processing_function=volume_processing_function,
ignore_mismatch_error=mismatch_error_ignorance,
pan_processing_function=panning_processing_function,
note_rtable_replacement=note_referance_table_replacement,
)
else:
@ -239,7 +247,7 @@ class MusicSequence:
verify: bool = True,
):
"""
从字节码导入音乐序列,目前支持 MSQ 第二、三版和 FSQ 第一版。
从字节码导入音乐序列,目前支持 MSQ 第二、三、四版和 FSQ 第一、二版。
Paramaters
==========
@ -250,7 +258,9 @@ class MusicSequence:
"""
if bytes_buffer_in[:4] == b"MSQ!":
if bytes_buffer_in[:4] in (b"MSQ!", b"MSQ$"):
note_format_v3 = bytes_buffer_in[0] == b"MSQ$"
group_1 = int.from_bytes(bytes_buffer_in[4:6], "big", signed=False)
group_2 = int.from_bytes(bytes_buffer_in[6:8], "big", signed=False)
@ -289,6 +299,11 @@ class MusicSequence:
code_buffer=bytes_buffer_in[stt_index:end_index],
is_high_time_precision=high_quantity,
)
if note_format_v3
else decode_note_bytes_v2(
bytes_buffer_in[stt_index:end_index],
is_high_time_precision=high_quantity,
)
)
channel_note_count += 1
stt_index = end_index
@ -371,7 +386,9 @@ class MusicSequence:
),
)
elif bytes_buffer_in[:4] == b"FSQ!":
elif bytes_buffer_in[:4] in (b"FSQ!", b"FSQ$"):
note_format_v3 = bytes_buffer_in[:4] == b"FSQ$"
group_1 = int.from_bytes(bytes_buffer_in[4:6], "big", signed=False)
group_2 = int.from_bytes(bytes_buffer_in[6:8], "big", signed=False)
@ -436,9 +453,16 @@ class MusicSequence:
+ high_quantity
+ (bytes_buffer_in[stt_index] >> 2)
)
_read_note = MineNote.decode(
code_buffer=bytes_buffer_in[stt_index:end_index],
is_high_time_precision=high_quantity,
_read_note = (
MineNote.decode(
code_buffer=bytes_buffer_in[stt_index:end_index],
is_high_time_precision=high_quantity,
)
if note_format_v3
else decode_note_bytes_v2(
code_buffer_bytes=bytes_buffer_in[stt_index:end_index],
is_high_time_precision=high_quantity,
)
)
stt_index = end_index
except Exception as _err:
@ -515,8 +539,8 @@ class MusicSequence:
+ (bytes_buffer_in[stt_index] >> 2)
)
channels_[channel_index].append(
MineNote.decode(
code_buffer=bytes_buffer_in[stt_index:end_index],
decode_note_bytes_v2(
code_buffer_bytes=bytes_buffer_in[stt_index:end_index],
is_high_time_precision=high_quantity,
)
)
@ -553,7 +577,7 @@ class MusicSequence:
try:
end_index = stt_index + 14 + (bytes_buffer_in[stt_index] >> 2)
channels_[channel_index].append(
MineNote.decode(bytes_buffer_in[stt_index:end_index])
decode_note_bytes_v1(bytes_buffer_in[stt_index:end_index])
)
stt_index = end_index
except:
@ -631,8 +655,12 @@ class MusicSequence:
# (已废弃)
# 第二版 MSQ 的码头: MSQ@ 字串编码: GB18030
#
# 第三版 MSQ 的码头: MSQ! 字串编码: GB18030 大端字节序
# 第一版 FSQ 的码头: FSQ!
# 第四版 MSQ 和 第二版 FSQ 的码头分别为 MSQ$ 和 FSQ$
# 其序列存储格式与第三版一致,但在每个音频的识别上做了调整
# 音频内容的调整见 subclass.py
# 音乐名称长度 6 位 支持到 63
# 最小音量 minimum_volume 10 位 最大支持 1023 即三位小数
@ -786,11 +814,13 @@ class MusicSequence:
midi: mido.MidiFile,
ignore_mismatch_error: bool = True,
speed: float = 1.0,
default_program_value: int = -1,
default_program_value: int = MIDI_DEFAULT_PROGRAM_VALUE,
default_volume_value: int = MIDI_DEFAULT_VOLUME_VALUE,
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,
vol_processing_function: FittingFunctionType = natural_curve,
pan_processing_function: FittingFunctionType = panning_2_rotation_trigonometric,
note_rtable_replacement: Dict[str, str] = {},
) -> Tuple[MineNoteChannelType, int, Dict[str, int]]:
"""
@ -804,6 +834,8 @@ class MusicSequence:
音乐播放速度倍数
default_program_value: int
默认的 MIDI 乐器值
default_volume_value: int
默认的通道音量值
default_tempo_value: int
默认的 MIDI TEMPO 值
pitched_note_rtable: Dict[int, Tuple[str, int]]
@ -811,7 +843,9 @@ class MusicSequence:
percussion_note_rtable: Dict[int, Tuple[str, int]]
打击乐器Midi-MC对照表
vol_processing_function: Callable[[float], float]
声像偏移拟合函数
音量对播放距离的拟合函数
pan_processing_function: Callable[[float], float]
声像偏移对播放旋转角度的拟合函数
note_rtable_replacement: Dict[str, str]
音符名称替换表,此表用于对 Minecraft 乐器名称进行替换,而非 Midi Program 的替换
@ -826,9 +860,15 @@ class MusicSequence:
# 一个midi中仅有16个通道 我们通过通道来识别而不是音轨
midi_channels: MineNoteChannelType = empty_midi_channels(default_staff=[])
channel_program: Dict[int, int] = empty_midi_channels(
default_staff=default_program_value
channel_controler: Dict[int, Dict[str, int]] = empty_midi_channels(
default_staff={
MIDI_PROGRAM: default_program_value,
MIDI_VOLUME: default_volume_value,
MIDI_PAN: 64,
}
)
tempo = default_tempo_value
note_count = 0
note_count_per_instrument: Dict[str, int] = {}
@ -863,76 +903,84 @@ class MusicSequence:
# 简化
if msg.type == "set_tempo":
tempo = msg.tempo
else:
if msg.type == "program_change":
channel_program[msg.channel] = msg.program
elif msg.type == "program_change":
channel_controler[msg.channel][MIDI_PROGRAM] = msg.program
elif msg.type == "note_on" and msg.velocity != 0:
note_queue_A[msg.channel].append(
(msg.note, channel_program[msg.channel])
elif msg.is_cc(7):
channel_controler[msg.channel][MIDI_VOLUME] = msg.value
elif msg.is_cc(10):
channel_controler[msg.channel][MIDI_PAN] = msg.value
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].append((msg.velocity, microseconds))
note_queue_B[msg.channel].remove((_velocity, _ms))
elif (msg.type == "note_off") or (
msg.type == "note_on" and msg.velocity == 0
):
if (msg.note, channel_program[msg.channel]) in note_queue_A[
msg.channel
]:
_velocity, _ms = note_queue_B[msg.channel][
note_queue_A[msg.channel].index(
(msg.note, channel_program[msg.channel])
)
]
note_queue_A[msg.channel].remove(
(msg.note, channel_program[msg.channel])
midi_channels[msg.channel].append(
that_note := midi_msgs_to_minenote(
inst_=(
msg.note
if msg.channel == 9
else channel_controler[msg.channel][MIDI_PROGRAM]
),
note_=(
channel_controler[msg.channel][MIDI_PROGRAM]
if msg.channel == 9
else msg.note
),
percussive_=(msg.channel == 9),
volume_=channel_controler[msg.channel][MIDI_VOLUME],
velocity_=_velocity,
panning_=channel_controler[msg.channel][MIDI_PAN],
start_time_=_ms, # 微秒
duration_=microseconds - _ms, # 微秒
play_speed=speed,
midi_reference_table=(
percussion_note_rtable
if msg.channel == 9
else pitched_note_rtable
),
volume_processing_method_=vol_processing_function,
panning_processing_method_=pan_processing_function,
note_table_replacement=note_rtable_replacement,
)
note_queue_B[msg.channel].remove((_velocity, _ms))
midi_channels[msg.channel].append(
that_note := midi_msgs_to_minenote(
inst_=(
msg.note
if msg.channel == 9
else channel_program[msg.channel]
),
note_=(
channel_program[msg.channel]
if msg.channel == 9
else msg.note
),
velocity_=_velocity,
start_time_=_ms, # 微秒
duration_=microseconds - _ms, # 微秒
percussive_=(msg.channel == 9),
play_speed=speed,
midi_reference_table=(
percussion_note_rtable
if msg.channel == 9
else pitched_note_rtable
),
volume_processing_method_=vol_processing_function,
note_table_replacement=note_rtable_replacement,
)
)
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
)
note_count += 1
if that_note.sound_name in note_count_per_instrument.keys():
note_count_per_instrument[that_note.sound_name] += 1
else:
if ignore_mismatch_error:
print(
"[WARRING] MIDI格式错误 音符不匹配 {} 无法在上文中找到与之匹配的音符开音消息".format(
msg
)
)
else:
raise NoteOnOffMismatchError(
"当前的MIDI很可能有损坏之嫌……",
msg,
"无法在上文中找到与之匹配的音符开音消息。",
note_count_per_instrument[that_note.sound_name] = 1
else:
if ignore_mismatch_error:
print(
"[WARRING] MIDI格式错误 音符不匹配 {} 无法在上文中找到与之匹配的音符开音消息".format(
msg
)
)
else:
raise NoteOnOffMismatchError(
"当前的MIDI很可能有损坏之嫌……",
msg,
"无法在上文中找到与之匹配的音符开音消息。",
)
"""整合后的音乐通道格式
每个通道包括若干消息元素其中逃不过这三种:
@ -985,12 +1033,14 @@ class MidiConvert(MusicSequence):
ignore_mismatch_error: bool = True,
playment_speed: float = 1,
default_midi_program_value: int = MIDI_DEFAULT_PROGRAM_VALUE,
default_midi_volume_value: int = MIDI_DEFAULT_VOLUME_VALUE,
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,
enable_old_exe_format: bool = False,
minimum_volume: float = 0.1,
vol_processing_function: FittingFunctionType = natural_curve,
pan_processing_function: FittingFunctionType = panning_2_rotation_trigonometric,
pitch_deviation: float = 0,
note_rtable_replacement: Dict[str, str] = {},
):
@ -1009,6 +1059,8 @@ class MidiConvert(MusicSequence):
音乐播放速度倍数
default_midi_program_value: int
默认的 MIDI Program 值,当 Midi 文件没有指定 Program 值时,使用此值
default_midi_volume_value: int
默认的 MIDI 音量值,当 Midi 文件没有指定此值时,使用此值
default_tempo_value: int
默认的 MIDI TEMPO 值,同上理
pitched_note_rtable: Dict[int, Tuple[str, int]]
@ -1020,7 +1072,9 @@ class MidiConvert(MusicSequence):
minimum_volume: float
最小播放音量
vol_processing_function: Callable[[float], float]
声像偏移拟合函数
音量对播放距离的拟合函数
pan_processing_function: Callable[[float], float]
声像偏移对播放旋转角度的拟合函数
pitch_deviation: float
音调偏移量,手动指定全曲音调偏移量
note_rtable_replacement: Dict[str, str]
@ -1044,11 +1098,13 @@ class MidiConvert(MusicSequence):
mismatch_error_ignorance=ignore_mismatch_error,
speed_multiplier=playment_speed,
default_midi_program=default_midi_program_value,
default_midi_volume=default_midi_volume_value,
default_tempo=default_tempo_value,
pitched_note_referance_table=pitched_note_rtable,
percussion_note_referance_table=percussion_note_rtable,
minimum_vol=minimum_volume,
volume_processing_function=vol_processing_function,
panning_processing_function=pan_processing_function,
deviation=pitch_deviation,
note_referance_table_replacement=note_rtable_replacement,
)
@ -1060,12 +1116,14 @@ class MidiConvert(MusicSequence):
mismatch_error_ignorance: bool = True,
play_speed: float = 1,
default_midi_program: int = MIDI_DEFAULT_PROGRAM_VALUE,
default_midi_volume: int = MIDI_DEFAULT_VOLUME_VALUE,
default_tempo: int = mido.midifiles.midifiles.DEFAULT_TEMPO,
pitched_note_table: MidiInstrumentTableType = MM_TOUCH_PITCHED_INSTRUMENT_TABLE,
percussion_note_table: MidiInstrumentTableType = MM_TOUCH_PERCUSSION_INSTRUMENT_TABLE,
old_exe_format: bool = False,
min_volume: float = 0.1,
vol_processing_func: FittingFunctionType = natural_curve,
pan_processing_func: FittingFunctionType = panning_2_rotation_linear,
music_pitch_deviation: float = 0,
note_table_replacement: Dict[str, str] = {},
):
@ -1082,6 +1140,8 @@ class MidiConvert(MusicSequence):
音乐播放速度倍数
default_midi_program: int
默认的 MIDI Program 值,当 Midi 文件没有指定 Program 值时,使用此值
default_midi_volume: int
默认每个通道的音量值,当 Midi 文件没有指定音量值时,使用此值
default_tempo: int
默认的MIDI TEMPO值
pitched_note_table: Dict[int, Tuple[str, int]]
@ -1093,7 +1153,9 @@ class MidiConvert(MusicSequence):
min_volume: float
最小播放音量
vol_processing_func: Callable[[float], float]
声像偏移拟合函数
音量对播放距离的拟合函数
pan_processing_func: Callable[[float], float]
声像偏移对播放旋转角度的拟合函数
music_pitch_deviation: float
全曲音符的音调偏移量
note_table_replacement: Dict[str, str]
@ -1115,12 +1177,14 @@ class MidiConvert(MusicSequence):
ignore_mismatch_error=mismatch_error_ignorance,
playment_speed=play_speed,
default_midi_program_value=default_midi_program,
default_midi_volume_value=default_midi_volume,
default_tempo_value=default_tempo,
pitched_note_rtable=pitched_note_table,
percussion_note_rtable=percussion_note_table,
enable_old_exe_format=old_exe_format,
minimum_volume=min_volume,
vol_processing_function=vol_processing_func,
pan_processing_function=pan_processing_func,
pitch_deviation=music_pitch_deviation,
note_rtable_replacement=note_table_replacement,
)

View File

@ -16,9 +16,9 @@ Terms & Conditions: License.md in the root directory
# Email TriM-Organization@hotmail.com
# 若需转载或借鉴 许可声明请查看仓库目录下的 License.md
from math import sin, cos, asin, radians, degrees, sqrt, atan
from dataclasses import dataclass
from typing import Optional, Any, List, Tuple, Union
from typing import Optional, Any, List, Tuple, Union, Dict
from .constants import MC_PITCHED_INSTRUMENT_LIST
@ -34,7 +34,7 @@ class MineNote:
"""midi音高"""
velocity: int
"""响度(力度)"""
"""力度"""
start_tick: int
"""开始之时 命令刻"""
@ -48,8 +48,11 @@ class MineNote:
percussive: bool
"""是否作为打击乐器启用"""
position_displacement: Tuple[float, float, float]
"""像位移"""
sound_distance: float
"""源距离"""
sound_azimuth: Tuple[float, float]
"""声源方位"""
extra_info: Any
"""你觉得放什么好?"""
@ -63,19 +66,26 @@ class MineNote:
last_time: int,
mass_precision_time: int = 0,
is_percussion: Optional[bool] = None,
displacement: Optional[Tuple[float, float, float]] = None,
extra_information: Optional[Any] = None,
distance: Optional[float] = None,
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` 开始之时(命令刻)
注:此处的时间是用从乐曲开始到当前的毫秒
注:此处的时间是用从乐曲开始到当前的
:param last_time:`int` 音符延续时间(命令刻)
:param mass_precision_time:`int` 高精度的开始时间偏移量(1/1250秒)
:param is_percussion:`bool` 是否作为打击乐器
:param displacement:`tuple[int,int,int]` 声像位移
:param distance: `float` 发声源距离玩家的距离(半径 `r`
注:距离越近,音量越高,默认为 0。此参数可以与音量成某种函数关系。
:param azimuth:`tuple[float, float]` 声源方位
此参数为tuple包含两个元素分别表示
`rV` 发声源在竖直(上下)轴上,从玩家视角正前方开始,向顺时针旋转的角度
`rH` 发声源在水平(左右)轴上,从玩家视角正前方开始,向上(到达玩家正上方顶点后变为向下,以此类推的旋转)旋转的角度
:param extra_information:`Any` 附加信息"""
self.sound_name: str = mc_sound_name
"""乐器ID"""
@ -97,13 +107,85 @@ class MineNote:
)
"""是否为打击乐器"""
self.position_displacement = (
(0, 0, 0) if (displacement is None) else displacement
self.sound_distance = (
(16 if distance > 16 else (distance if distance > 0 else 0))
if distance
else 0
)
"""像位移"""
"""源距离"""
self.sound_azimuth = (azimuth[0] % 360, azimuth[1] % 360) if azimuth else (0, 0)
"""声源方位"""
self.extra_info = extra_information
@classmethod
def from_traditional(
cls,
mc_sound_name: str,
midi_pitch: Optional[int],
midi_velocity: int,
start_time: int,
last_time: int,
mass_precision_time: int = 0,
is_percussion: Optional[bool] = None,
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` 开始之时(命令刻)
注:此处的时间是用从乐曲开始到当前的刻数
: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` 附加信息"""
if displacement is None:
displacement = (0, 0, 0)
r = 0
alpha_v = 0
beta_h = 0
else:
r = sqrt(displacement[0] ** 2 + displacement[1] ** 2 + displacement[2] ** 2)
if r == 0:
alpha_v = 0
beta_h = 0
else:
beta_h = round(degrees(asin(displacement[1] / r)), 8)
if displacement[2] == 0:
alpha_v = -90 if displacement[0] > 0 else 90
else:
alpha_v = round(
degrees(atan(-displacement[0] / displacement[2])), 8
)
return cls(
mc_sound_name=mc_sound_name,
midi_pitch=midi_pitch,
midi_velocity=midi_velocity,
start_time=start_time,
last_time=last_time,
mass_precision_time=mass_precision_time,
is_percussion=is_percussion,
distance=r,
azimuth=(alpha_v, beta_h),
extra_information=extra_information,
)
@property
def position_displacement(self) -> Tuple[float, float, float]:
"""声像位移"""
dk1 = self.sound_distance * round(cos(radians(self.sound_azimuth[1])), 8)
return (
-dk1 * round(sin(radians(self.sound_azimuth[0])), 8),
self.sound_distance * round(sin(radians(self.sound_azimuth[1])), 8),
dk1 * round(cos(radians(self.sound_azimuth[0])), 8),
)
@classmethod
def decode(cls, code_buffer: bytes, is_high_time_precision: bool = True):
"""自字节码析出MineNote类"""
@ -115,39 +197,25 @@ class MineNote:
sound_name_length = group_1 >> 7
if code_buffer[6] & 0b1:
position_displacement_ = (
int.from_bytes(
(
code_buffer[8 + sound_name_length : 10 + sound_name_length]
if is_high_time_precision
else code_buffer[7 + sound_name_length : 9 + sound_name_length]
),
"big",
)
/ 1000,
int.from_bytes(
(
code_buffer[10 + sound_name_length : 12 + sound_name_length]
if is_high_time_precision
else code_buffer[9 + sound_name_length : 11 + sound_name_length]
),
"big",
)
/ 1000,
int.from_bytes(
(
code_buffer[12 + sound_name_length : 14 + sound_name_length]
if is_high_time_precision
else code_buffer[
11 + sound_name_length : 13 + sound_name_length
]
),
"big",
)
/ 1000,
distance_ = (
code_buffer[8 + sound_name_length]
if is_high_time_precision
else code_buffer[7 + sound_name_length]
) / 15
group_2 = int.from_bytes(
(
code_buffer[9 + sound_name_length : 14 + sound_name_length]
if is_high_time_precision
else code_buffer[8 + sound_name_length : 13 + sound_name_length]
),
"big",
)
azimuth_ = ((group_2 >> 20) / 2912, (group_2 & 0xFFFFF) / 2912)
else:
position_displacement_ = (0, 0, 0)
distance_ = 0
azimuth_ = (0, 0)
try:
return cls(
@ -164,7 +232,8 @@ class MineNote:
last_time=duration_,
mass_precision_time=code_buffer[7] if is_high_time_precision else 0,
is_percussion=percussive_,
displacement=position_displacement_,
distance=distance_,
azimuth=azimuth_,
)
except:
print(code_buffer, "\n", o)
@ -182,6 +251,8 @@ class MineNote:
:return bytes 打包好的字节码
"""
# MineNote 的字节码共有三个顺次版本分别如下
# 字符串长度 6 位 支持到 63
# note_pitch 7 位 支持到 127
# start_tick 17 位 支持到 131071 即 109.22583 分钟 合 1.8204305 小时
@ -202,8 +273,12 @@ class MineNote:
# 第一版编码: UTF-8
# 第二版编码: GB18030
# +++
# (在第三版中已废弃)
# position_displacement 每个元素长 16 位 合 2 字节
# 共 48 位 合 6 字节 支持存储三位小数和两位整数,其值必须在 [0, 65.535] 之间
# (在第三版中新增)
# sound_distance 8 位 支持到 255 即 16 格 合 1 字节(按值放大 15 倍存储,精度可达 1 / 15
# sound_azimuth 每个元素长 20 位 共 40 位 合 5 字节。每个值放大 2912 倍存储,即支持到 360.08756868131866 度,精度同理
return (
(
@ -245,9 +320,11 @@ class MineNote:
+ r
+ (
(
round(self.position_displacement[0] * 1000).to_bytes(2, "big")
+ round(self.position_displacement[1] * 1000).to_bytes(2, "big")
+ round(self.position_displacement[2] * 1000).to_bytes(2, "big")
round(self.sound_distance * 15).to_bytes(1, "big")
+ (
(round(self.sound_azimuth[0] * 2912) << 20)
+ round(self.sound_azimuth[1] * 2912)
).to_bytes(5, "big")
)
if is_displacement_included
else b""
@ -258,24 +335,32 @@ class MineNote:
"""设置附加信息"""
self.extra_info = sth
def __str__(self, is_displacement: bool = False):
return "{}Note(Instrument = {}, {}Velocity = {}, StartTick = {}, Duration = {}{}{})".format(
"Percussive" if self.percussive else "",
self.sound_name,
"" if self.percussive else "NotePitch = {}, ".format(self.note_pitch),
self.velocity,
self.start_tick,
self.duration,
(
", PositionDisplacement = {}".format(self.position_displacement)
if is_displacement
def stringize(
self, include_displacement: bool = False, include_extra_data: bool = False
) -> str:
return (
"{}Note(Instrument = {}, {}Velocity = {}, StartTick = {}, Duration = {}{}".format(
"Percussive" if self.percussive else "",
self.sound_name,
"" if self.percussive else "NotePitch = {}, ".format(self.note_pitch),
self.velocity,
self.start_tick,
self.duration,
)
+ (
", SoundDistance = `r`{}, SoundAzimuth = (`αV`{}, `βH`{})".format(
self.sound_distance, *self.sound_azimuth
)
if include_displacement
else ""
),
)
+ (", ExtraData = {}".format(self.extra_info) if include_extra_data else "")
+ ")"
)
def tuplize(self, is_displacement: bool = False):
tuplized = self.__tuple__()
return tuplized[:-2] + ((tuplized[-1],) if is_displacement else ())
return tuplized[:-2] + (tuplized[-2:] if is_displacement else ())
def __list__(self) -> List:
return (
@ -285,7 +370,8 @@ class MineNote:
self.velocity,
self.start_tick,
self.duration,
self.position_displacement,
self.sound_distance,
self.sound_azimuth,
]
if self.percussive
else [
@ -295,15 +381,16 @@ class MineNote:
self.velocity,
self.start_tick,
self.duration,
self.position_displacement,
self.sound_distance,
self.sound_azimuth,
]
)
def __tuple__(
self,
) -> Union[
Tuple[bool, str, int, int, int, int, Tuple[float, float, float]],
Tuple[bool, str, int, int, int, Tuple[float, float, float]],
Tuple[bool, str, int, int, int, int, float, Tuple[float, float]],
Tuple[bool, str, int, int, int, float, Tuple[float, float]],
]:
return (
(
@ -312,7 +399,8 @@ class MineNote:
self.velocity,
self.start_tick,
self.duration,
self.position_displacement,
self.sound_distance,
self.sound_azimuth,
)
if self.percussive
else (
@ -322,7 +410,8 @@ class MineNote:
self.velocity,
self.start_tick,
self.duration,
self.position_displacement,
self.sound_distance,
self.sound_azimuth,
)
)
@ -334,7 +423,9 @@ class MineNote:
"Velocity": self.velocity,
"StartTick": self.start_tick,
"Duration": self.duration,
"PositionDisplacement": self.position_displacement,
"SoundDistance": self.sound_distance,
"SoundAzimuth": self.sound_azimuth,
"ExtraData": self.extra_info,
}
if self.percussive
else {
@ -344,7 +435,9 @@ class MineNote:
"Velocity": self.velocity,
"StartTick": self.start_tick,
"Duration": self.duration,
"PositionDisplacement": self.position_displacement,
"SoundDistance": self.sound_distance,
"SoundAzimuth": self.sound_azimuth,
"ExtraData": self.extra_info,
}
)

View File

@ -32,7 +32,7 @@ Midi乐器对照表类型
FittingFunctionType = Callable[[float], float]
"""
声像偏移音量拟合函数类型
拟合函数类型
"""
ChannelType = Dict[

View File

@ -42,7 +42,7 @@ from .constants import (
)
from .exceptions import MusicSequenceDecodeError
from .subclass import MineNote, mctick2timestr
from .types import MidiInstrumentTableType, MineNoteChannelType
from .types import MidiInstrumentTableType, MineNoteChannelType, FittingFunctionType
def empty_midi_channels(
@ -171,6 +171,47 @@ def straight_line(vol: float) -> float:
return vol / -8 + 16
def panning_2_rotation_linear(pan_: float) -> float:
"""
Midi 左右平衡偏移值线性转为声源旋转角度
Parameters
----------
pan_: int
Midi 左右平衡偏移值
此参数为int范围从0到127当为 64 时,声源居中
Returns
-------
float
声源旋转角度
"""
return (pan_ - 64) * 90 / 63
def panning_2_rotation_trigonometric(pan_: float) -> float:
"""
Midi 左右平衡偏移值,依照圆的声场定位,转为声源旋转角度
Parameters
----------
pan_: int
Midi 左右平衡偏移值
此参数为int范围从0到127当为 64 时,声源居中
Returns
-------
float
声源旋转角度
"""
if pan_ <= 0:
return -90
elif pan_ >= 127:
return 90
else:
return math.degrees(math.acos((64 - pan_) / 63)) - 90
def minenote_to_command_paramaters(
note_: MineNote,
pitch_deviation: float = 0,
@ -252,12 +293,15 @@ def midi_msgs_to_minenote(
inst_: int, # 乐器编号
note_: int,
percussive_: bool, # 是否作为打击乐器启用
volume_: int,
velocity_: int,
panning_: int,
start_time_: int,
duration_: int,
play_speed: float,
midi_reference_table: MidiInstrumentTableType,
volume_processing_method_: Callable[[float], float],
volume_processing_method_: FittingFunctionType,
panning_processing_method_: FittingFunctionType,
note_table_replacement: Dict[str, str] = {},
) -> MineNote:
"""
@ -265,12 +309,15 @@ def midi_msgs_to_minenote(
:param inst_: int 乐器编号
:param note_: int 音高编号(音符编号)
:param percussive_: bool 是否作为打击乐器启用
:param velocity_: int 力度(响度)
: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_proccessing_method_: Callable[[float], float] 音量处理函数
:param volume_processing_method_: Callable[[float], float] 音量处理函数
:param panning_processing_method_: Callable[[float], float] 立体声相偏移处理函数
:param note_table_replacement: Dict[str, str] 音符替换表,定义 Minecraft 音符字串的替换
:return MineNote我的世界音符对象
@ -281,8 +328,6 @@ def midi_msgs_to_minenote(
"note.bd" if percussive_ else "note.flute",
)
mc_distance_volume = volume_processing_method_(velocity_)
return MineNote(
mc_sound_name=note_table_replacement.get(mc_sound_ID, mc_sound_ID),
midi_pitch=note_,
@ -291,7 +336,8 @@ def midi_msgs_to_minenote(
last_time=round(duration_ / float(play_speed) / 50000),
mass_precision_time=round((start_time_ / float(play_speed) - tk * 50000) / 800),
is_percussion=percussive_,
displacement=(0, mc_distance_volume, 0),
distance=volume_processing_method_(volume_),
azimuth=(panning_processing_method_(panning_), 0),
)
@ -299,12 +345,15 @@ def midi_msgs_to_minenote_using_kami_respack(
inst_: int, # 乐器编号
note_: int,
percussive_: bool, # 是否作为打击乐器启用
volume_: int,
velocity_: int,
panning_: int,
start_time_: int,
duration_: int,
play_speed: float,
midi_reference_table: MidiInstrumentTableType,
volume_processing_method_: Callable[[float], float],
panning_processing_method_: FittingFunctionType,
note_table_replacement: Dict[str, str] = {},
) -> MineNote:
"""
@ -312,12 +361,15 @@ def midi_msgs_to_minenote_using_kami_respack(
:param inst_: int 乐器编号
:param note_: int 音高编号(音符编号)
:param percussive_: bool 是否作为打击乐器启用
:param velocity_: int 力度(响度)
: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_proccessing_method_: Callable[[float], float] 音量处理函数
:param volume_processing_method_: Callable[[float], float] 音量处理函数
:param panning_processing_method_: Callable[[float], float] 立体声相偏移处理函数
:param note_table_replacement: Dict[str, str] 音符替换表,定义 Minecraft 音符字串的替换
:return MineNote我的世界音符对象
@ -327,7 +379,9 @@ def midi_msgs_to_minenote_using_kami_respack(
if not percussive_ and (0 <= inst_ <= 119):
mc_sound_ID = "{}{}.{}".format(
# inst_, "d" if duration_ < 500_000 else "c", note_
inst_, "d", note_
inst_,
"d",
note_,
)
elif percussive_ and (27 <= inst_ <= 87):
mc_sound_ID = "-1d.{}".format(inst_)
@ -339,8 +393,6 @@ def midi_msgs_to_minenote_using_kami_respack(
"note.bd" if percussive_ else "note.flute",
)
mc_distance_volume = volume_processing_method_(velocity_)
return MineNote(
mc_sound_name=note_table_replacement.get(mc_sound_ID, mc_sound_ID),
midi_pitch=note_ if using_original else 1,
@ -349,11 +401,14 @@ def midi_msgs_to_minenote_using_kami_respack(
last_time=round(duration_ / float(play_speed) / 50000),
mass_precision_time=round((start_time_ / float(play_speed) - tk * 50000) / 800),
is_percussion=percussive_,
displacement=(0, mc_distance_volume, 0),
distance=volume_processing_method_(volume_),
azimuth=(panning_processing_method_(panning_), 0),
extra_information={
"USING_ORIGINAL_SOUND": using_original, # 判断 extra_information 中是否有 USING_ORIGINAL_SOUND 键是判断是否使用神羽资源包解析的一个显著方法
"INST_VALUE": note_ if percussive_ else inst_,
"NOTE_VALUE": inst_ if percussive_ else note_,
"VOLUME_VALUE": volume_,
"PIN_VALUE": panning_,
},
)
@ -456,7 +511,7 @@ def soundID_to_blockID(
def load_decode_musicsequence_metainfo(
buffer_in: BinaryIO,
) -> Tuple[str, float, float, bool, int]:
) -> Tuple[str, float, float, bool, int, bool]:
"""
以流的方式解码音乐序列元信息
@ -468,10 +523,10 @@ def load_decode_musicsequence_metainfo(
Returns
-------
Tuple[str, float, float, bool, int]
音乐名称,最小音量,音调偏移,是否启用高精度,最后的流指针位置
音乐名称,最小音量,音调偏移,是否启用高精度,最后的流指针位置是否使用新的音符存储格式MineNote第三版
"""
buffer_in.seek(4, 0)
note_format_v3 = buffer_in.read(4) in (b"MSQ$", b"FSQ$")
group_1 = int.from_bytes(buffer_in.read(2), "big")
group_2 = int.from_bytes(buffer_in.read(2), "big", signed=False)
@ -490,6 +545,7 @@ def load_decode_musicsequence_metainfo(
),
bool(group_2 & 0b1000000000000000),
stt_index + 8,
note_format_v3,
)
@ -497,6 +553,7 @@ def load_decode_fsq_flush_release(
buffer_in: BinaryIO,
starter_index: int,
high_quantity_note: bool,
new_note_format: bool,
) -> Generator[MineNote, Any, None]:
"""
以流的方式解码FSQ音乐序列的音符序列并流式返回
@ -509,6 +566,8 @@ def load_decode_fsq_flush_release(
字节流中,音符序列的起始索引
high_quantity_note : bool
是否启用高精度音符解析
new_note_format : bool
是否启用新音符格式解析MineNote第三版
Returns
-------
@ -540,9 +599,16 @@ def load_decode_fsq_flush_release(
12 + high_quantity_note + ((_first_byte := (buffer_in.read(1)))[0] >> 2)
)
yield MineNote.decode(
code_buffer=_first_byte + buffer_in.read(_note_bytes_length),
is_high_time_precision=high_quantity_note,
yield (
MineNote.decode(
code_buffer=_first_byte + buffer_in.read(_note_bytes_length),
is_high_time_precision=high_quantity_note,
)
if new_note_format
else decode_note_bytes_v2(
code_buffer_bytes=_first_byte + buffer_in.read(_note_bytes_length),
is_high_time_precision=high_quantity_note,
)
)
except Exception as _err:
# print(bytes_buffer_in[stt_index:end_index])
@ -557,6 +623,7 @@ def load_decode_msq_flush_release(
buffer_in: BinaryIO,
starter_index: int,
high_quantity_note: bool,
new_note_format: bool,
) -> Generator[Tuple[int, MineNote], Any, None]:
"""以流的方式解码MSQ音乐序列的音符序列并流式返回
@ -568,6 +635,8 @@ def load_decode_msq_flush_release(
字节流中,音符序列的起始索引
high_quantity_note : bool
是否启用高精度音符解析
new_note_format : bool
是否启用新音符格式解析MineNote第三版
Returns
-------
@ -703,9 +772,18 @@ def load_decode_msq_flush_release(
# print("读取音符字节串", _bytes_buffer_in[_stt_index:_end_index])
_read_in_note_list.append(
(
MineNote.decode(
code_buffer=_bytes_buffer_in[_stt_index:_end_index],
is_high_time_precision=high_quantity_note,
(
MineNote.decode(
code_buffer=_bytes_buffer_in[_stt_index:_end_index],
is_high_time_precision=high_quantity_note,
)
if new_note_format
else decode_note_bytes_v2(
code_buffer_bytes=_bytes_buffer_in[
_stt_index:_end_index
],
is_high_time_precision=high_quantity_note,
)
),
__channel_index,
)
@ -802,3 +880,128 @@ def guess_deviation(
/ total_instrument_count
/ total_note_count
)
# 延长支持用
def decode_note_bytes_v1(
code_buffer_bytes: bytes,
) -> MineNote:
"""使用第一版的 MineNote 字节码标准析出MineNote类"""
group_1 = int.from_bytes(code_buffer_bytes[:6], "big")
percussive_ = bool(group_1 & 0b1)
duration_ = (group_1 := group_1 >> 1) & 0b11111111111111111
start_tick_ = (group_1 := group_1 >> 17) & 0b11111111111111111
note_pitch_ = (group_1 := group_1 >> 17) & 0b1111111
sound_name_length = group_1 >> 7
if code_buffer_bytes[6] & 0b1:
position_displacement_ = (
int.from_bytes(
code_buffer_bytes[8 + sound_name_length : 10 + sound_name_length],
"big",
)
/ 1000,
int.from_bytes(
code_buffer_bytes[10 + sound_name_length : 12 + sound_name_length],
"big",
)
/ 1000,
int.from_bytes(
code_buffer_bytes[12 + sound_name_length : 14 + sound_name_length],
"big",
)
/ 1000,
)
else:
position_displacement_ = (0, 0, 0)
try:
return MineNote.from_traditional(
mc_sound_name=code_buffer_bytes[8 : 8 + sound_name_length].decode(
encoding="utf-8"
),
midi_pitch=note_pitch_,
midi_velocity=code_buffer_bytes[6] >> 1,
start_time=start_tick_,
last_time=duration_,
is_percussion=percussive_,
displacement=position_displacement_,
extra_information={"track_number": code_buffer_bytes[7]},
)
except:
print(code_buffer_bytes, "\n", code_buffer_bytes[8 : 8 + sound_name_length])
raise
def decode_note_bytes_v2(
code_buffer_bytes: bytes, is_high_time_precision: bool = True
) -> MineNote:
"""使用第二版的 MineNote 字节码标准析出MineNote类"""
group_1 = int.from_bytes(code_buffer_bytes[:6], "big")
percussive_ = bool(group_1 & 0b1)
duration_ = (group_1 := group_1 >> 1) & 0b11111111111111111
start_tick_ = (group_1 := group_1 >> 17) & 0b11111111111111111
note_pitch_ = (group_1 := group_1 >> 17) & 0b1111111
sound_name_length = group_1 >> 7
if code_buffer_bytes[6] & 0b1:
position_displacement_ = (
int.from_bytes(
(
code_buffer_bytes[8 + sound_name_length : 10 + sound_name_length]
if is_high_time_precision
else code_buffer_bytes[
7 + sound_name_length : 9 + sound_name_length
]
),
"big",
)
/ 1000,
int.from_bytes(
(
code_buffer_bytes[10 + sound_name_length : 12 + sound_name_length]
if is_high_time_precision
else code_buffer_bytes[
9 + sound_name_length : 11 + sound_name_length
]
),
"big",
)
/ 1000,
int.from_bytes(
(
code_buffer_bytes[12 + sound_name_length : 14 + sound_name_length]
if is_high_time_precision
else code_buffer_bytes[
11 + sound_name_length : 13 + sound_name_length
]
),
"big",
)
/ 1000,
)
else:
position_displacement_ = (0, 0, 0)
try:
return MineNote.from_traditional(
mc_sound_name=(
o := (
code_buffer_bytes[8 : 8 + sound_name_length]
if is_high_time_precision
else code_buffer_bytes[7 : 7 + sound_name_length]
)
).decode(encoding="GB18030"),
midi_pitch=note_pitch_,
midi_velocity=code_buffer_bytes[6] >> 1,
start_time=start_tick_,
last_time=duration_,
mass_precision_time=code_buffer_bytes[7] if is_high_time_precision else 0,
is_percussion=percussive_,
displacement=position_displacement_,
)
except:
print(code_buffer_bytes, "\n", o)
raise

View File

@ -93,3 +93,6 @@
source = "file"
path = "Musicreater/__init__.py"
[tool.pyright]
typeCheckingMode = "basic"

View File

@ -31,7 +31,7 @@ with open("test.fsq", "rb") as f:
pprint(metas := load_decode_musicsequence_metainfo(f))
pprint("流式 FSQ 音符序列:")
cnt = 0
for i in load_decode_fsq_flush_release(f, metas[-1], metas[-2]):
for i in load_decode_fsq_flush_release(f, metas[-2], metas[-3], metas[-1]):
pprint(
i,
)

View File

@ -12,8 +12,8 @@ print(
"乐器使用情况",
)
for name in set(
sorted(
for name in sorted(
set(
[
n.split(".")[0].replace("c", "").replace("d", "")
for n in msct.note_count_per_instrument.keys()

View File

@ -30,5 +30,5 @@ with open("test.msq", "rb") as f:
pprint("流式 MSQ 元数据:")
pprint(metas := load_decode_musicsequence_metainfo(f))
pprint("流式 MSQ 音符序列:")
for i in load_decode_msq_flush_release(f, metas[-1], metas[-2]):
for i in load_decode_msq_flush_release(f, metas[-2], metas[-3], metas[-1]):
pprint(i)