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

View File

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

View File

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

View File

@ -164,11 +164,13 @@ class MusicSequence:
mismatch_error_ignorance: bool = True, mismatch_error_ignorance: bool = True,
speed_multiplier: float = 1, speed_multiplier: float = 1,
default_midi_program: int = MIDI_DEFAULT_PROGRAM_VALUE, default_midi_program: int = MIDI_DEFAULT_PROGRAM_VALUE,
default_midi_volume: int = MIDI_DEFAULT_VOLUME_VALUE,
default_tempo: int = mido.midifiles.midifiles.DEFAULT_TEMPO, default_tempo: int = mido.midifiles.midifiles.DEFAULT_TEMPO,
pitched_note_referance_table: MidiInstrumentTableType = MM_TOUCH_PITCHED_INSTRUMENT_TABLE, pitched_note_referance_table: MidiInstrumentTableType = MM_TOUCH_PITCHED_INSTRUMENT_TABLE,
percussion_note_referance_table: MidiInstrumentTableType = MM_TOUCH_PERCUSSION_INSTRUMENT_TABLE, percussion_note_referance_table: MidiInstrumentTableType = MM_TOUCH_PERCUSSION_INSTRUMENT_TABLE,
minimum_vol: float = 0.1, minimum_vol: float = 0.1,
volume_processing_function: FittingFunctionType = natural_curve, volume_processing_function: FittingFunctionType = natural_curve,
panning_processing_function: FittingFunctionType = panning_2_rotation_linear,
deviation: float = 0, deviation: float = 0,
note_referance_table_replacement: Dict[str, str] = {}, note_referance_table_replacement: Dict[str, str] = {},
): ):
@ -177,7 +179,7 @@ class MusicSequence:
Paramaters Paramaters
========== ==========
mido_file: mido.MidiFile 对象 mido_file: mido.MidiFile
需要处理的midi对象 需要处理的midi对象
midi_music_name: str midi_music_name: str
音乐名称 音乐名称
@ -186,7 +188,9 @@ class MusicSequence:
speed_multiplier: float speed_multiplier: float
音乐播放速度倍数 音乐播放速度倍数
default_midi_program: int default_midi_program: int
默认的MIDI Program值 默认的 MIDI Program值
default_midi_volume: int
默认的 MIDI 音量
default_tempo: int default_tempo: int
默认的MIDI TEMPO值 默认的MIDI TEMPO值
pitched_note_referance_table: Dict[int, Tuple[str, int]] pitched_note_referance_table: Dict[int, Tuple[str, int]]
@ -196,7 +200,9 @@ class MusicSequence:
minimum_vol: float minimum_vol: float
播放的最小音量 应为 (0,1] 范围内的小数 播放的最小音量 应为 (0,1] 范围内的小数
volume_processing_function: Callable[[float], float] volume_processing_function: Callable[[float], float]
声像偏移拟合函数 音量对播放距离的拟合函数
panning_processing_function: Callable[[float], float]
声像偏移对播放旋转角度的拟合函数
deviation: float deviation: float
全曲音调偏移值 全曲音调偏移值
note_referance_table_replacement: Dict[str, str] note_referance_table_replacement: Dict[str, str]
@ -210,13 +216,15 @@ class MusicSequence:
inst_note_count, inst_note_count,
) = cls.to_music_note_channels( ) = cls.to_music_note_channels(
midi=mido_file, midi=mido_file,
ignore_mismatch_error=mismatch_error_ignorance,
speed=speed_multiplier, 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, pitched_note_rtable=pitched_note_referance_table,
percussion_note_rtable=percussion_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, 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, note_rtable_replacement=note_referance_table_replacement,
) )
else: else:
@ -239,7 +247,7 @@ class MusicSequence:
verify: bool = True, verify: bool = True,
): ):
""" """
从字节码导入音乐序列,目前支持 MSQ 第二、三版和 FSQ 第一版。 从字节码导入音乐序列,目前支持 MSQ 第二、三、四版和 FSQ 第一、二版。
Paramaters 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_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) 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], code_buffer=bytes_buffer_in[stt_index:end_index],
is_high_time_precision=high_quantity, 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 channel_note_count += 1
stt_index = end_index 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_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) group_2 = int.from_bytes(bytes_buffer_in[6:8], "big", signed=False)
@ -436,9 +453,16 @@ class MusicSequence:
+ high_quantity + high_quantity
+ (bytes_buffer_in[stt_index] >> 2) + (bytes_buffer_in[stt_index] >> 2)
) )
_read_note = MineNote.decode( _read_note = (
code_buffer=bytes_buffer_in[stt_index:end_index], MineNote.decode(
is_high_time_precision=high_quantity, 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 stt_index = end_index
except Exception as _err: except Exception as _err:
@ -515,8 +539,8 @@ class MusicSequence:
+ (bytes_buffer_in[stt_index] >> 2) + (bytes_buffer_in[stt_index] >> 2)
) )
channels_[channel_index].append( channels_[channel_index].append(
MineNote.decode( decode_note_bytes_v2(
code_buffer=bytes_buffer_in[stt_index:end_index], code_buffer_bytes=bytes_buffer_in[stt_index:end_index],
is_high_time_precision=high_quantity, is_high_time_precision=high_quantity,
) )
) )
@ -553,7 +577,7 @@ class MusicSequence:
try: try:
end_index = stt_index + 14 + (bytes_buffer_in[stt_index] >> 2) end_index = stt_index + 14 + (bytes_buffer_in[stt_index] >> 2)
channels_[channel_index].append( 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 stt_index = end_index
except: except:
@ -631,8 +655,12 @@ class MusicSequence:
# (已废弃) # (已废弃)
# 第二版 MSQ 的码头: MSQ@ 字串编码: GB18030 # 第二版 MSQ 的码头: MSQ@ 字串编码: GB18030
#
# 第三版 MSQ 的码头: MSQ! 字串编码: GB18030 大端字节序 # 第三版 MSQ 的码头: MSQ! 字串编码: GB18030 大端字节序
# 第一版 FSQ 的码头: FSQ! # 第一版 FSQ 的码头: FSQ!
# 第四版 MSQ 和 第二版 FSQ 的码头分别为 MSQ$ 和 FSQ$
# 其序列存储格式与第三版一致,但在每个音频的识别上做了调整
# 音频内容的调整见 subclass.py
# 音乐名称长度 6 位 支持到 63 # 音乐名称长度 6 位 支持到 63
# 最小音量 minimum_volume 10 位 最大支持 1023 即三位小数 # 最小音量 minimum_volume 10 位 最大支持 1023 即三位小数
@ -786,11 +814,13 @@ class MusicSequence:
midi: mido.MidiFile, midi: mido.MidiFile,
ignore_mismatch_error: bool = True, ignore_mismatch_error: bool = True,
speed: float = 1.0, 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, default_tempo_value: int = mido.midifiles.midifiles.DEFAULT_TEMPO,
pitched_note_rtable: MidiInstrumentTableType = MM_TOUCH_PITCHED_INSTRUMENT_TABLE, pitched_note_rtable: MidiInstrumentTableType = MM_TOUCH_PITCHED_INSTRUMENT_TABLE,
percussion_note_rtable: MidiInstrumentTableType = MM_TOUCH_PERCUSSION_INSTRUMENT_TABLE, percussion_note_rtable: MidiInstrumentTableType = MM_TOUCH_PERCUSSION_INSTRUMENT_TABLE,
vol_processing_function: FittingFunctionType = natural_curve, vol_processing_function: FittingFunctionType = natural_curve,
pan_processing_function: FittingFunctionType = panning_2_rotation_trigonometric,
note_rtable_replacement: Dict[str, str] = {}, note_rtable_replacement: Dict[str, str] = {},
) -> Tuple[MineNoteChannelType, int, Dict[str, int]]: ) -> Tuple[MineNoteChannelType, int, Dict[str, int]]:
""" """
@ -804,6 +834,8 @@ class MusicSequence:
音乐播放速度倍数 音乐播放速度倍数
default_program_value: int default_program_value: int
默认的 MIDI 乐器值 默认的 MIDI 乐器值
default_volume_value: int
默认的通道音量值
default_tempo_value: int default_tempo_value: int
默认的 MIDI TEMPO 值 默认的 MIDI TEMPO 值
pitched_note_rtable: Dict[int, Tuple[str, int]] pitched_note_rtable: Dict[int, Tuple[str, int]]
@ -811,7 +843,9 @@ class MusicSequence:
percussion_note_rtable: Dict[int, Tuple[str, int]] percussion_note_rtable: Dict[int, Tuple[str, int]]
打击乐器Midi-MC对照表 打击乐器Midi-MC对照表
vol_processing_function: Callable[[float], float] vol_processing_function: Callable[[float], float]
声像偏移拟合函数 音量对播放距离的拟合函数
pan_processing_function: Callable[[float], float]
声像偏移对播放旋转角度的拟合函数
note_rtable_replacement: Dict[str, str] note_rtable_replacement: Dict[str, str]
音符名称替换表,此表用于对 Minecraft 乐器名称进行替换,而非 Midi Program 的替换 音符名称替换表,此表用于对 Minecraft 乐器名称进行替换,而非 Midi Program 的替换
@ -826,9 +860,15 @@ class MusicSequence:
# 一个midi中仅有16个通道 我们通过通道来识别而不是音轨 # 一个midi中仅有16个通道 我们通过通道来识别而不是音轨
midi_channels: MineNoteChannelType = empty_midi_channels(default_staff=[]) 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 tempo = default_tempo_value
note_count = 0 note_count = 0
note_count_per_instrument: Dict[str, int] = {} note_count_per_instrument: Dict[str, int] = {}
@ -863,76 +903,84 @@ class MusicSequence:
# 简化 # 简化
if msg.type == "set_tempo": if msg.type == "set_tempo":
tempo = msg.tempo tempo = msg.tempo
else: elif msg.type == "program_change":
if msg.type == "program_change": channel_controler[msg.channel][MIDI_PROGRAM] = msg.program
channel_program[msg.channel] = msg.program
elif msg.type == "note_on" and msg.velocity != 0: elif msg.is_cc(7):
note_queue_A[msg.channel].append( channel_controler[msg.channel][MIDI_VOLUME] = msg.value
(msg.note, channel_program[msg.channel]) 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 ( midi_channels[msg.channel].append(
msg.type == "note_on" and msg.velocity == 0 that_note := midi_msgs_to_minenote(
): inst_=(
if (msg.note, channel_program[msg.channel]) in note_queue_A[ msg.note
msg.channel if msg.channel == 9
]: else channel_controler[msg.channel][MIDI_PROGRAM]
_velocity, _ms = note_queue_B[msg.channel][ ),
note_queue_A[msg.channel].index( note_=(
(msg.note, channel_program[msg.channel]) channel_controler[msg.channel][MIDI_PROGRAM]
) if msg.channel == 9
] else msg.note
note_queue_A[msg.channel].remove( ),
(msg.note, channel_program[msg.channel]) 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)) )
note_count += 1
midi_channels[msg.channel].append( if that_note.sound_name in note_count_per_instrument.keys():
that_note := midi_msgs_to_minenote( note_count_per_instrument[that_note.sound_name] += 1
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
else: else:
if ignore_mismatch_error: note_count_per_instrument[that_note.sound_name] = 1
print( else:
"[WARRING] MIDI格式错误 音符不匹配 {} 无法在上文中找到与之匹配的音符开音消息".format( if ignore_mismatch_error:
msg print(
) "[WARRING] MIDI格式错误 音符不匹配 {} 无法在上文中找到与之匹配的音符开音消息".format(
) msg
else:
raise NoteOnOffMismatchError(
"当前的MIDI很可能有损坏之嫌……",
msg,
"无法在上文中找到与之匹配的音符开音消息。",
) )
)
else:
raise NoteOnOffMismatchError(
"当前的MIDI很可能有损坏之嫌……",
msg,
"无法在上文中找到与之匹配的音符开音消息。",
)
"""整合后的音乐通道格式 """整合后的音乐通道格式
每个通道包括若干消息元素其中逃不过这三种: 每个通道包括若干消息元素其中逃不过这三种:
@ -985,12 +1033,14 @@ class MidiConvert(MusicSequence):
ignore_mismatch_error: bool = True, ignore_mismatch_error: bool = True,
playment_speed: float = 1, playment_speed: float = 1,
default_midi_program_value: int = MIDI_DEFAULT_PROGRAM_VALUE, 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, default_tempo_value: int = mido.midifiles.midifiles.DEFAULT_TEMPO,
pitched_note_rtable: MidiInstrumentTableType = MM_TOUCH_PITCHED_INSTRUMENT_TABLE, pitched_note_rtable: MidiInstrumentTableType = MM_TOUCH_PITCHED_INSTRUMENT_TABLE,
percussion_note_rtable: MidiInstrumentTableType = MM_TOUCH_PERCUSSION_INSTRUMENT_TABLE, percussion_note_rtable: MidiInstrumentTableType = MM_TOUCH_PERCUSSION_INSTRUMENT_TABLE,
enable_old_exe_format: bool = False, enable_old_exe_format: bool = False,
minimum_volume: float = 0.1, minimum_volume: float = 0.1,
vol_processing_function: FittingFunctionType = natural_curve, vol_processing_function: FittingFunctionType = natural_curve,
pan_processing_function: FittingFunctionType = panning_2_rotation_trigonometric,
pitch_deviation: float = 0, pitch_deviation: float = 0,
note_rtable_replacement: Dict[str, str] = {}, note_rtable_replacement: Dict[str, str] = {},
): ):
@ -1009,6 +1059,8 @@ class MidiConvert(MusicSequence):
音乐播放速度倍数 音乐播放速度倍数
default_midi_program_value: int default_midi_program_value: int
默认的 MIDI Program 值,当 Midi 文件没有指定 Program 值时,使用此值 默认的 MIDI Program 值,当 Midi 文件没有指定 Program 值时,使用此值
default_midi_volume_value: int
默认的 MIDI 音量值,当 Midi 文件没有指定此值时,使用此值
default_tempo_value: int default_tempo_value: int
默认的 MIDI TEMPO 值,同上理 默认的 MIDI TEMPO 值,同上理
pitched_note_rtable: Dict[int, Tuple[str, int]] pitched_note_rtable: Dict[int, Tuple[str, int]]
@ -1020,7 +1072,9 @@ class MidiConvert(MusicSequence):
minimum_volume: float minimum_volume: float
最小播放音量 最小播放音量
vol_processing_function: Callable[[float], float] vol_processing_function: Callable[[float], float]
声像偏移拟合函数 音量对播放距离的拟合函数
pan_processing_function: Callable[[float], float]
声像偏移对播放旋转角度的拟合函数
pitch_deviation: float pitch_deviation: float
音调偏移量,手动指定全曲音调偏移量 音调偏移量,手动指定全曲音调偏移量
note_rtable_replacement: Dict[str, str] note_rtable_replacement: Dict[str, str]
@ -1044,11 +1098,13 @@ class MidiConvert(MusicSequence):
mismatch_error_ignorance=ignore_mismatch_error, mismatch_error_ignorance=ignore_mismatch_error,
speed_multiplier=playment_speed, speed_multiplier=playment_speed,
default_midi_program=default_midi_program_value, default_midi_program=default_midi_program_value,
default_midi_volume=default_midi_volume_value,
default_tempo=default_tempo_value, default_tempo=default_tempo_value,
pitched_note_referance_table=pitched_note_rtable, pitched_note_referance_table=pitched_note_rtable,
percussion_note_referance_table=percussion_note_rtable, percussion_note_referance_table=percussion_note_rtable,
minimum_vol=minimum_volume, minimum_vol=minimum_volume,
volume_processing_function=vol_processing_function, volume_processing_function=vol_processing_function,
panning_processing_function=pan_processing_function,
deviation=pitch_deviation, deviation=pitch_deviation,
note_referance_table_replacement=note_rtable_replacement, note_referance_table_replacement=note_rtable_replacement,
) )
@ -1060,12 +1116,14 @@ class MidiConvert(MusicSequence):
mismatch_error_ignorance: bool = True, mismatch_error_ignorance: bool = True,
play_speed: float = 1, play_speed: float = 1,
default_midi_program: int = MIDI_DEFAULT_PROGRAM_VALUE, default_midi_program: int = MIDI_DEFAULT_PROGRAM_VALUE,
default_midi_volume: int = MIDI_DEFAULT_VOLUME_VALUE,
default_tempo: int = mido.midifiles.midifiles.DEFAULT_TEMPO, default_tempo: int = mido.midifiles.midifiles.DEFAULT_TEMPO,
pitched_note_table: MidiInstrumentTableType = MM_TOUCH_PITCHED_INSTRUMENT_TABLE, pitched_note_table: MidiInstrumentTableType = MM_TOUCH_PITCHED_INSTRUMENT_TABLE,
percussion_note_table: MidiInstrumentTableType = MM_TOUCH_PERCUSSION_INSTRUMENT_TABLE, percussion_note_table: MidiInstrumentTableType = MM_TOUCH_PERCUSSION_INSTRUMENT_TABLE,
old_exe_format: bool = False, old_exe_format: bool = False,
min_volume: float = 0.1, min_volume: float = 0.1,
vol_processing_func: FittingFunctionType = natural_curve, vol_processing_func: FittingFunctionType = natural_curve,
pan_processing_func: FittingFunctionType = panning_2_rotation_linear,
music_pitch_deviation: float = 0, music_pitch_deviation: float = 0,
note_table_replacement: Dict[str, str] = {}, note_table_replacement: Dict[str, str] = {},
): ):
@ -1082,6 +1140,8 @@ class MidiConvert(MusicSequence):
音乐播放速度倍数 音乐播放速度倍数
default_midi_program: int default_midi_program: int
默认的 MIDI Program 值,当 Midi 文件没有指定 Program 值时,使用此值 默认的 MIDI Program 值,当 Midi 文件没有指定 Program 值时,使用此值
default_midi_volume: int
默认每个通道的音量值,当 Midi 文件没有指定音量值时,使用此值
default_tempo: int default_tempo: int
默认的MIDI TEMPO值 默认的MIDI TEMPO值
pitched_note_table: Dict[int, Tuple[str, int]] pitched_note_table: Dict[int, Tuple[str, int]]
@ -1093,7 +1153,9 @@ class MidiConvert(MusicSequence):
min_volume: float min_volume: float
最小播放音量 最小播放音量
vol_processing_func: Callable[[float], float] vol_processing_func: Callable[[float], float]
声像偏移拟合函数 音量对播放距离的拟合函数
pan_processing_func: Callable[[float], float]
声像偏移对播放旋转角度的拟合函数
music_pitch_deviation: float music_pitch_deviation: float
全曲音符的音调偏移量 全曲音符的音调偏移量
note_table_replacement: Dict[str, str] note_table_replacement: Dict[str, str]
@ -1115,12 +1177,14 @@ class MidiConvert(MusicSequence):
ignore_mismatch_error=mismatch_error_ignorance, ignore_mismatch_error=mismatch_error_ignorance,
playment_speed=play_speed, playment_speed=play_speed,
default_midi_program_value=default_midi_program, default_midi_program_value=default_midi_program,
default_midi_volume_value=default_midi_volume,
default_tempo_value=default_tempo, default_tempo_value=default_tempo,
pitched_note_rtable=pitched_note_table, pitched_note_rtable=pitched_note_table,
percussion_note_rtable=percussion_note_table, percussion_note_rtable=percussion_note_table,
enable_old_exe_format=old_exe_format, enable_old_exe_format=old_exe_format,
minimum_volume=min_volume, minimum_volume=min_volume,
vol_processing_function=vol_processing_func, vol_processing_function=vol_processing_func,
pan_processing_function=pan_processing_func,
pitch_deviation=music_pitch_deviation, pitch_deviation=music_pitch_deviation,
note_rtable_replacement=note_table_replacement, 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 # Email TriM-Organization@hotmail.com
# 若需转载或借鉴 许可声明请查看仓库目录下的 License.md # 若需转载或借鉴 许可声明请查看仓库目录下的 License.md
from math import sin, cos, asin, radians, degrees, sqrt, atan
from dataclasses import dataclass 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 from .constants import MC_PITCHED_INSTRUMENT_LIST
@ -34,7 +34,7 @@ class MineNote:
"""midi音高""" """midi音高"""
velocity: int velocity: int
"""响度(力度)""" """力度"""
start_tick: int start_tick: int
"""开始之时 命令刻""" """开始之时 命令刻"""
@ -48,8 +48,11 @@ class MineNote:
percussive: bool percussive: bool
"""是否作为打击乐器启用""" """是否作为打击乐器启用"""
position_displacement: Tuple[float, float, float] sound_distance: float
"""像位移""" """源距离"""
sound_azimuth: Tuple[float, float]
"""声源方位"""
extra_info: Any extra_info: Any
"""你觉得放什么好?""" """你觉得放什么好?"""
@ -63,19 +66,26 @@ class MineNote:
last_time: int, last_time: int,
mass_precision_time: int = 0, mass_precision_time: int = 0,
is_percussion: Optional[bool] = None, is_percussion: Optional[bool] = None,
displacement: Optional[Tuple[float, float, float]] = None, distance: Optional[float] = None,
extra_information: Optional[Any] = None, azimuth: Optional[Tuple[float, float]] = None,
extra_information: Optional[Dict[str, Any]] = None,
): ):
"""用于存储单个音符的类 """用于存储单个音符的类
:param mc_sound_name:`str` 《我的世界》声音ID :param mc_sound_name:`str` 《我的世界》声音ID
:param midi_pitch:`int` midi音高 :param midi_pitch:`int` midi音高
:param midi_velocity:`int` midi响度(力度) :param midi_velocity:`int` midi响度(力度)
:param start_time:`int` 开始之时(命令刻) :param start_time:`int` 开始之时(命令刻)
注:此处的时间是用从乐曲开始到当前的毫秒 注:此处的时间是用从乐曲开始到当前的
:param last_time:`int` 音符延续时间(命令刻) :param last_time:`int` 音符延续时间(命令刻)
:param mass_precision_time:`int` 高精度的开始时间偏移量(1/1250秒) :param mass_precision_time:`int` 高精度的开始时间偏移量(1/1250秒)
:param is_percussion:`bool` 是否作为打击乐器 :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` 附加信息""" :param extra_information:`Any` 附加信息"""
self.sound_name: str = mc_sound_name self.sound_name: str = mc_sound_name
"""乐器ID""" """乐器ID"""
@ -97,13 +107,85 @@ class MineNote:
) )
"""是否为打击乐器""" """是否为打击乐器"""
self.position_displacement = ( self.sound_distance = (
(0, 0, 0) if (displacement is None) else displacement (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 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 @classmethod
def decode(cls, code_buffer: bytes, is_high_time_precision: bool = True): def decode(cls, code_buffer: bytes, is_high_time_precision: bool = True):
"""自字节码析出MineNote类""" """自字节码析出MineNote类"""
@ -115,39 +197,25 @@ class MineNote:
sound_name_length = group_1 >> 7 sound_name_length = group_1 >> 7
if code_buffer[6] & 0b1: if code_buffer[6] & 0b1:
position_displacement_ = ( distance_ = (
int.from_bytes( code_buffer[8 + sound_name_length]
( if is_high_time_precision
code_buffer[8 + sound_name_length : 10 + sound_name_length] else code_buffer[7 + sound_name_length]
if is_high_time_precision ) / 15
else code_buffer[7 + sound_name_length : 9 + sound_name_length]
), group_2 = int.from_bytes(
"big", (
) code_buffer[9 + sound_name_length : 14 + sound_name_length]
/ 1000, if is_high_time_precision
int.from_bytes( else code_buffer[8 + sound_name_length : 13 + sound_name_length]
( ),
code_buffer[10 + sound_name_length : 12 + sound_name_length] "big",
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,
) )
azimuth_ = ((group_2 >> 20) / 2912, (group_2 & 0xFFFFF) / 2912)
else: else:
position_displacement_ = (0, 0, 0) distance_ = 0
azimuth_ = (0, 0)
try: try:
return cls( return cls(
@ -164,7 +232,8 @@ class MineNote:
last_time=duration_, last_time=duration_,
mass_precision_time=code_buffer[7] if is_high_time_precision else 0, mass_precision_time=code_buffer[7] if is_high_time_precision else 0,
is_percussion=percussive_, is_percussion=percussive_,
displacement=position_displacement_, distance=distance_,
azimuth=azimuth_,
) )
except: except:
print(code_buffer, "\n", o) print(code_buffer, "\n", o)
@ -182,6 +251,8 @@ class MineNote:
:return bytes 打包好的字节码 :return bytes 打包好的字节码
""" """
# MineNote 的字节码共有三个顺次版本分别如下
# 字符串长度 6 位 支持到 63 # 字符串长度 6 位 支持到 63
# note_pitch 7 位 支持到 127 # note_pitch 7 位 支持到 127
# start_tick 17 位 支持到 131071 即 109.22583 分钟 合 1.8204305 小时 # start_tick 17 位 支持到 131071 即 109.22583 分钟 合 1.8204305 小时
@ -202,8 +273,12 @@ class MineNote:
# 第一版编码: UTF-8 # 第一版编码: UTF-8
# 第二版编码: GB18030 # 第二版编码: GB18030
# +++ # +++
# (在第三版中已废弃)
# position_displacement 每个元素长 16 位 合 2 字节 # position_displacement 每个元素长 16 位 合 2 字节
# 共 48 位 合 6 字节 支持存储三位小数和两位整数,其值必须在 [0, 65.535] 之间 # 共 48 位 合 6 字节 支持存储三位小数和两位整数,其值必须在 [0, 65.535] 之间
# (在第三版中新增)
# sound_distance 8 位 支持到 255 即 16 格 合 1 字节(按值放大 15 倍存储,精度可达 1 / 15
# sound_azimuth 每个元素长 20 位 共 40 位 合 5 字节。每个值放大 2912 倍存储,即支持到 360.08756868131866 度,精度同理
return ( return (
( (
@ -245,9 +320,11 @@ class MineNote:
+ r + r
+ ( + (
( (
round(self.position_displacement[0] * 1000).to_bytes(2, "big") round(self.sound_distance * 15).to_bytes(1, "big")
+ round(self.position_displacement[1] * 1000).to_bytes(2, "big") + (
+ round(self.position_displacement[2] * 1000).to_bytes(2, "big") (round(self.sound_azimuth[0] * 2912) << 20)
+ round(self.sound_azimuth[1] * 2912)
).to_bytes(5, "big")
) )
if is_displacement_included if is_displacement_included
else b"" else b""
@ -258,24 +335,32 @@ class MineNote:
"""设置附加信息""" """设置附加信息"""
self.extra_info = sth self.extra_info = sth
def __str__(self, is_displacement: bool = False): def stringize(
return "{}Note(Instrument = {}, {}Velocity = {}, StartTick = {}, Duration = {}{}{})".format( self, include_displacement: bool = False, include_extra_data: bool = False
"Percussive" if self.percussive else "", ) -> str:
self.sound_name, return (
"" if self.percussive else "NotePitch = {}, ".format(self.note_pitch), "{}Note(Instrument = {}, {}Velocity = {}, StartTick = {}, Duration = {}{}".format(
self.velocity, "Percussive" if self.percussive else "",
self.start_tick, self.sound_name,
self.duration, "" if self.percussive else "NotePitch = {}, ".format(self.note_pitch),
( self.velocity,
", PositionDisplacement = {}".format(self.position_displacement) self.start_tick,
if is_displacement self.duration,
)
+ (
", SoundDistance = `r`{}, SoundAzimuth = (`αV`{}, `βH`{})".format(
self.sound_distance, *self.sound_azimuth
)
if include_displacement
else "" else ""
), )
+ (", ExtraData = {}".format(self.extra_info) if include_extra_data else "")
+ ")"
) )
def tuplize(self, is_displacement: bool = False): def tuplize(self, is_displacement: bool = False):
tuplized = self.__tuple__() 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: def __list__(self) -> List:
return ( return (
@ -285,7 +370,8 @@ class MineNote:
self.velocity, self.velocity,
self.start_tick, self.start_tick,
self.duration, self.duration,
self.position_displacement, self.sound_distance,
self.sound_azimuth,
] ]
if self.percussive if self.percussive
else [ else [
@ -295,15 +381,16 @@ class MineNote:
self.velocity, self.velocity,
self.start_tick, self.start_tick,
self.duration, self.duration,
self.position_displacement, self.sound_distance,
self.sound_azimuth,
] ]
) )
def __tuple__( def __tuple__(
self, self,
) -> Union[ ) -> Union[
Tuple[bool, str, int, int, int, int, Tuple[float, float, float]], Tuple[bool, str, int, int, int, int, float, Tuple[float, float]],
Tuple[bool, str, int, int, int, Tuple[float, float, float]], Tuple[bool, str, int, int, int, float, Tuple[float, float]],
]: ]:
return ( return (
( (
@ -312,7 +399,8 @@ class MineNote:
self.velocity, self.velocity,
self.start_tick, self.start_tick,
self.duration, self.duration,
self.position_displacement, self.sound_distance,
self.sound_azimuth,
) )
if self.percussive if self.percussive
else ( else (
@ -322,7 +410,8 @@ class MineNote:
self.velocity, self.velocity,
self.start_tick, self.start_tick,
self.duration, self.duration,
self.position_displacement, self.sound_distance,
self.sound_azimuth,
) )
) )
@ -334,7 +423,9 @@ class MineNote:
"Velocity": self.velocity, "Velocity": self.velocity,
"StartTick": self.start_tick, "StartTick": self.start_tick,
"Duration": self.duration, "Duration": self.duration,
"PositionDisplacement": self.position_displacement, "SoundDistance": self.sound_distance,
"SoundAzimuth": self.sound_azimuth,
"ExtraData": self.extra_info,
} }
if self.percussive if self.percussive
else { else {
@ -344,7 +435,9 @@ class MineNote:
"Velocity": self.velocity, "Velocity": self.velocity,
"StartTick": self.start_tick, "StartTick": self.start_tick,
"Duration": self.duration, "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] FittingFunctionType = Callable[[float], float]
""" """
声像偏移音量拟合函数类型 拟合函数类型
""" """
ChannelType = Dict[ ChannelType = Dict[

View File

@ -42,7 +42,7 @@ from .constants import (
) )
from .exceptions import MusicSequenceDecodeError from .exceptions import MusicSequenceDecodeError
from .subclass import MineNote, mctick2timestr from .subclass import MineNote, mctick2timestr
from .types import MidiInstrumentTableType, MineNoteChannelType from .types import MidiInstrumentTableType, MineNoteChannelType, FittingFunctionType
def empty_midi_channels( def empty_midi_channels(
@ -171,6 +171,47 @@ def straight_line(vol: float) -> float:
return vol / -8 + 16 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( def minenote_to_command_paramaters(
note_: MineNote, note_: MineNote,
pitch_deviation: float = 0, pitch_deviation: float = 0,
@ -252,12 +293,15 @@ def midi_msgs_to_minenote(
inst_: int, # 乐器编号 inst_: int, # 乐器编号
note_: int, note_: int,
percussive_: bool, # 是否作为打击乐器启用 percussive_: bool, # 是否作为打击乐器启用
volume_: int,
velocity_: int, velocity_: int,
panning_: int,
start_time_: int, start_time_: int,
duration_: int, duration_: int,
play_speed: float, play_speed: float,
midi_reference_table: MidiInstrumentTableType, midi_reference_table: MidiInstrumentTableType,
volume_processing_method_: Callable[[float], float], volume_processing_method_: FittingFunctionType,
panning_processing_method_: FittingFunctionType,
note_table_replacement: Dict[str, str] = {}, note_table_replacement: Dict[str, str] = {},
) -> MineNote: ) -> MineNote:
""" """
@ -265,12 +309,15 @@ def midi_msgs_to_minenote(
:param inst_: int 乐器编号 :param inst_: int 乐器编号
:param note_: int 音高编号(音符编号) :param note_: int 音高编号(音符编号)
:param percussive_: bool 是否作为打击乐器启用 :param percussive_: bool 是否作为打击乐器启用
:param velocity_: int 力度(响度) :param volume_: int 音量
:param velocity_: int 力度
:param panning_: int 声相偏移
:param start_time_: int 音符起始时间(微秒) :param start_time_: int 音符起始时间(微秒)
:param duration_: int 音符持续时间(微秒) :param duration_: int 音符持续时间(微秒)
:param play_speed: float 曲目播放速度 :param play_speed: float 曲目播放速度
:param midi_reference_table: Dict[int, str] 转换对照表 :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 音符字串的替换 :param note_table_replacement: Dict[str, str] 音符替换表,定义 Minecraft 音符字串的替换
:return MineNote我的世界音符对象 :return MineNote我的世界音符对象
@ -281,8 +328,6 @@ def midi_msgs_to_minenote(
"note.bd" if percussive_ else "note.flute", "note.bd" if percussive_ else "note.flute",
) )
mc_distance_volume = volume_processing_method_(velocity_)
return MineNote( return MineNote(
mc_sound_name=note_table_replacement.get(mc_sound_ID, mc_sound_ID), mc_sound_name=note_table_replacement.get(mc_sound_ID, mc_sound_ID),
midi_pitch=note_, midi_pitch=note_,
@ -291,7 +336,8 @@ def midi_msgs_to_minenote(
last_time=round(duration_ / float(play_speed) / 50000), last_time=round(duration_ / float(play_speed) / 50000),
mass_precision_time=round((start_time_ / float(play_speed) - tk * 50000) / 800), mass_precision_time=round((start_time_ / float(play_speed) - tk * 50000) / 800),
is_percussion=percussive_, 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, # 乐器编号 inst_: int, # 乐器编号
note_: int, note_: int,
percussive_: bool, # 是否作为打击乐器启用 percussive_: bool, # 是否作为打击乐器启用
volume_: int,
velocity_: int, velocity_: int,
panning_: int,
start_time_: int, start_time_: int,
duration_: int, duration_: int,
play_speed: float, play_speed: float,
midi_reference_table: MidiInstrumentTableType, midi_reference_table: MidiInstrumentTableType,
volume_processing_method_: Callable[[float], float], volume_processing_method_: Callable[[float], float],
panning_processing_method_: FittingFunctionType,
note_table_replacement: Dict[str, str] = {}, note_table_replacement: Dict[str, str] = {},
) -> MineNote: ) -> MineNote:
""" """
@ -312,12 +361,15 @@ def midi_msgs_to_minenote_using_kami_respack(
:param inst_: int 乐器编号 :param inst_: int 乐器编号
:param note_: int 音高编号(音符编号) :param note_: int 音高编号(音符编号)
:param percussive_: bool 是否作为打击乐器启用 :param percussive_: bool 是否作为打击乐器启用
:param velocity_: int 力度(响度) :param volume_: int 音量
:param velocity_: int 力度
:param panning_: int 声相偏移
:param start_time_: int 音符起始时间(微秒) :param start_time_: int 音符起始时间(微秒)
:param duration_: int 音符持续时间(微秒) :param duration_: int 音符持续时间(微秒)
:param play_speed: float 曲目播放速度 :param play_speed: float 曲目播放速度
:param midi_reference_table: Dict[int, str] 转换对照表 :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 音符字串的替换 :param note_table_replacement: Dict[str, str] 音符替换表,定义 Minecraft 音符字串的替换
:return MineNote我的世界音符对象 :return MineNote我的世界音符对象
@ -327,7 +379,9 @@ def midi_msgs_to_minenote_using_kami_respack(
if not percussive_ and (0 <= inst_ <= 119): if not percussive_ and (0 <= inst_ <= 119):
mc_sound_ID = "{}{}.{}".format( mc_sound_ID = "{}{}.{}".format(
# inst_, "d" if duration_ < 500_000 else "c", note_ # inst_, "d" if duration_ < 500_000 else "c", note_
inst_, "d", note_ inst_,
"d",
note_,
) )
elif percussive_ and (27 <= inst_ <= 87): elif percussive_ and (27 <= inst_ <= 87):
mc_sound_ID = "-1d.{}".format(inst_) 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", "note.bd" if percussive_ else "note.flute",
) )
mc_distance_volume = volume_processing_method_(velocity_)
return MineNote( return MineNote(
mc_sound_name=note_table_replacement.get(mc_sound_ID, mc_sound_ID), mc_sound_name=note_table_replacement.get(mc_sound_ID, mc_sound_ID),
midi_pitch=note_ if using_original else 1, 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), last_time=round(duration_ / float(play_speed) / 50000),
mass_precision_time=round((start_time_ / float(play_speed) - tk * 50000) / 800), mass_precision_time=round((start_time_ / float(play_speed) - tk * 50000) / 800),
is_percussion=percussive_, is_percussion=percussive_,
displacement=(0, mc_distance_volume, 0), distance=volume_processing_method_(volume_),
azimuth=(panning_processing_method_(panning_), 0),
extra_information={ extra_information={
"USING_ORIGINAL_SOUND": using_original, # 判断 extra_information 中是否有 USING_ORIGINAL_SOUND 键是判断是否使用神羽资源包解析的一个显著方法 "USING_ORIGINAL_SOUND": using_original, # 判断 extra_information 中是否有 USING_ORIGINAL_SOUND 键是判断是否使用神羽资源包解析的一个显著方法
"INST_VALUE": note_ if percussive_ else inst_, "INST_VALUE": note_ if percussive_ else inst_,
"NOTE_VALUE": inst_ if percussive_ else note_, "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( def load_decode_musicsequence_metainfo(
buffer_in: BinaryIO, 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 Returns
------- -------
Tuple[str, float, float, bool, int] 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_1 = int.from_bytes(buffer_in.read(2), "big")
group_2 = int.from_bytes(buffer_in.read(2), "big", signed=False) 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), bool(group_2 & 0b1000000000000000),
stt_index + 8, stt_index + 8,
note_format_v3,
) )
@ -497,6 +553,7 @@ def load_decode_fsq_flush_release(
buffer_in: BinaryIO, buffer_in: BinaryIO,
starter_index: int, starter_index: int,
high_quantity_note: bool, high_quantity_note: bool,
new_note_format: bool,
) -> Generator[MineNote, Any, None]: ) -> Generator[MineNote, Any, None]:
""" """
以流的方式解码FSQ音乐序列的音符序列并流式返回 以流的方式解码FSQ音乐序列的音符序列并流式返回
@ -509,6 +566,8 @@ def load_decode_fsq_flush_release(
字节流中,音符序列的起始索引 字节流中,音符序列的起始索引
high_quantity_note : bool high_quantity_note : bool
是否启用高精度音符解析 是否启用高精度音符解析
new_note_format : bool
是否启用新音符格式解析MineNote第三版
Returns Returns
------- -------
@ -540,9 +599,16 @@ def load_decode_fsq_flush_release(
12 + high_quantity_note + ((_first_byte := (buffer_in.read(1)))[0] >> 2) 12 + high_quantity_note + ((_first_byte := (buffer_in.read(1)))[0] >> 2)
) )
yield MineNote.decode( yield (
code_buffer=_first_byte + buffer_in.read(_note_bytes_length), MineNote.decode(
is_high_time_precision=high_quantity_note, 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: except Exception as _err:
# print(bytes_buffer_in[stt_index:end_index]) # print(bytes_buffer_in[stt_index:end_index])
@ -557,6 +623,7 @@ def load_decode_msq_flush_release(
buffer_in: BinaryIO, buffer_in: BinaryIO,
starter_index: int, starter_index: int,
high_quantity_note: bool, high_quantity_note: bool,
new_note_format: bool,
) -> Generator[Tuple[int, MineNote], Any, None]: ) -> Generator[Tuple[int, MineNote], Any, None]:
"""以流的方式解码MSQ音乐序列的音符序列并流式返回 """以流的方式解码MSQ音乐序列的音符序列并流式返回
@ -568,6 +635,8 @@ def load_decode_msq_flush_release(
字节流中,音符序列的起始索引 字节流中,音符序列的起始索引
high_quantity_note : bool high_quantity_note : bool
是否启用高精度音符解析 是否启用高精度音符解析
new_note_format : bool
是否启用新音符格式解析MineNote第三版
Returns Returns
------- -------
@ -703,9 +772,18 @@ def load_decode_msq_flush_release(
# print("读取音符字节串", _bytes_buffer_in[_stt_index:_end_index]) # print("读取音符字节串", _bytes_buffer_in[_stt_index:_end_index])
_read_in_note_list.append( _read_in_note_list.append(
( (
MineNote.decode( (
code_buffer=_bytes_buffer_in[_stt_index:_end_index], MineNote.decode(
is_high_time_precision=high_quantity_note, 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, __channel_index,
) )
@ -802,3 +880,128 @@ def guess_deviation(
/ total_instrument_count / total_instrument_count
/ total_note_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" source = "file"
path = "Musicreater/__init__.py" 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(metas := load_decode_musicsequence_metainfo(f))
pprint("流式 FSQ 音符序列:") pprint("流式 FSQ 音符序列:")
cnt = 0 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( pprint(
i, i,
) )

View File

@ -12,8 +12,8 @@ print(
"乐器使用情况", "乐器使用情况",
) )
for name in set( for name in sorted(
sorted( set(
[ [
n.split(".")[0].replace("c", "").replace("d", "") n.split(".")[0].replace("c", "").replace("d", "")
for n in msct.note_count_per_instrument.keys() 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("流式 MSQ 元数据:")
pprint(metas := load_decode_musicsequence_metainfo(f)) pprint(metas := load_decode_musicsequence_metainfo(f))
pprint("流式 MSQ 音符序列:") 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) pprint(i)