mirror of
https://github.com/TriM-Organization/Musicreater.git
synced 2025-09-03 19:06:23 +00:00
Merge branch 'master' of https://gitee.com/EillesWan/Musicreater
This commit is contained in:
@ -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",
|
||||
|
@ -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 长笛
|
||||
)
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
)
|
||||
|
@ -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,
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -32,7 +32,7 @@ Midi乐器对照表类型
|
||||
|
||||
FittingFunctionType = Callable[[float], float]
|
||||
"""
|
||||
声像偏移音量拟合函数类型
|
||||
拟合函数类型
|
||||
"""
|
||||
|
||||
ChannelType = Dict[
|
||||
|
@ -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
|
||||
|
@ -93,3 +93,6 @@
|
||||
source = "file"
|
||||
path = "Musicreater/__init__.py"
|
||||
|
||||
|
||||
[tool.pyright]
|
||||
typeCheckingMode = "basic"
|
||||
|
@ -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,
|
||||
)
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
Reference in New Issue
Block a user