完成 Midi 导入插件移植

This commit is contained in:
2026-02-12 13:24:46 +08:00
parent 2a5ccb8eeb
commit fff8e43f53
23 changed files with 1254 additions and 377 deletions

View File

@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
存储 音·创 v3 的插件基类,提供抽象接口以供实际插件使用 音·创 v3 的插件基类,提供抽象接口以供实际插件使用
""" """
""" """
@@ -42,6 +42,7 @@ from typing import (
Iterator, Iterator,
Set, Set,
Type, Type,
Mapping,
) )
if sys.version_info >= (3, 11): if sys.version_info >= (3, 11):
@@ -132,7 +133,7 @@ class PluginConfig(ABC):
return {k: v for k, v in self.__dict__.items() if not k.startswith("_")} return {k: v for k, v in self.__dict__.items() if not k.startswith("_")}
@classmethod @classmethod
def from_dict(cls, data: Dict[str, Any]) -> "PluginConfig": def from_dict(cls, data: Mapping[str, Any]) -> "PluginConfig":
"""从字典创建配置实例 """从字典创建配置实例
参数 参数

32
Musicreater/_utils.py Normal file
View File

@@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
"""
音·创 v3 的功能性内容合辑
"""
"""
版权所有 © 2026 金羿、玉衡Alioth
Copyright © 2026 Eilles, YuhengAlioth
开源相关声明请见 仓库根目录下的 License.md
Terms & Conditions: License.md in the root directory
"""
# 睿乐组织 开发交流群 861684859
# Email TriM-Organization@hotmail.com
# 若需转载或借鉴 许可声明请查看仓库目录下的 License.md
from copy import deepcopy, copy
from typing import Any, Dict, Generator, List, Optional, Tuple, Union, TypeVar
T = TypeVar("T")
def enumerated_stuffcopy_dictionary(
enumeration_times: int = 17, staff: T = {}
) -> Dict[int, T]:
"""
生成一个字典其中键从0到enumeration_times-1值是staff的拷贝
"""
# 这告诉我们你不能忽略任何一个复制的序列因为它真的我哭死折磨我一整天全在这个bug上了
# 上面的这指的是 copy.deepcopy —— 金羿 来自 20260210
return {i: deepcopy(staff) for i in range(enumeration_times)}

View File

@@ -592,6 +592,8 @@ MM_DISLINK_PERCUSSION_INSTRUMENT_TABLE: Dict[int, str] = {
# NoteBlockStudio “NBS”音色对照表 # NoteBlockStudio “NBS”音色对照表
# https://github.com/OpenNBS/NoteBlockStudio/blob/main/scripts/midi_instruments/midi_instruments.gml # https://github.com/OpenNBS/NoteBlockStudio/blob/main/scripts/midi_instruments/midi_instruments.gml
# 此表来自于 Commit 1ab5357c197872495197f27ad8374d711b2a5195
# 需要更新https://github.com/OpenNBS/NoteBlockStudio/compare/main...development?diff=unified&w
MM_NBS_PITCHED_INSTRUMENT_TABLE: Dict[int, str] = { MM_NBS_PITCHED_INSTRUMENT_TABLE: Dict[int, str] = {
0: "note.harp", 0: "note.harp",

View File

@@ -0,0 +1,69 @@
# -*- coding: utf-8 -*-
"""
音·创 v3 内置的 Midi 读取插件用到的一些报错类型
"""
"""
版权所有 © 2026 金羿 & 玉衡Alioth
Copyright © 2026 Eilles & YuhengAlioth
开源相关声明请见 仓库根目录下的 License.md
Terms & Conditions: License.md in the root directory
"""
# 睿乐组织 开发交流群 861684859
# Email TriM-Organization@hotmail.com
# 若需转载或借鉴 许可声明请查看仓库目录下的 License.md
from Musicreater.exceptions import MusicreaterOuterlyError
class MidiFormatError(MusicreaterOuterlyError):
"""音·创 的所有MIDI格式错误均继承于此"""
def __init__(self, *args):
"""音·创 的所有MIDI格式错误均继承于此"""
super().__init__("MIDI 格式错误 - ", *args)
class NotDefineTempoError(MidiFormatError):
"""没有Tempo设定导致时间无法计算的错误"""
def __init__(self, *args):
"""没有Tempo设定导致时间无法计算的错误"""
super().__init__("在曲目开始时没有声明 Tempo未指定拍长", *args)
class ChannelOverFlowError(MidiFormatError):
"""一个midi中含有过多的通道"""
def __init__(self, max_channel=16, *args):
"""一个midi中含有过多的通道"""
super().__init__("含有过多的通道(数量应≤{}".format(max_channel), *args)
class NotDefineProgramError(MidiFormatError):
"""没有Program设定导致没有乐器可以选择的错误"""
def __init__(self, *args):
"""没有Program设定导致没有乐器可以选择的错误"""
super().__init__("未指定演奏乐器:", *args)
class NoteOnOffMismatchError(MidiFormatError):
"""音符开音和停止不匹配的错误"""
def __init__(self, *args):
"""音符开音和停止不匹配的错误"""
super().__init__("音符不匹配:", *args)
class LyricMismatchError(MidiFormatError):
"""歌词匹配解析错误"""
def __init__(self, *args):
"""有可能产生了错误的歌词解析"""
super().__init__("歌词解析错误:", *args)

View File

@@ -19,10 +19,11 @@ Terms & Conditions: License.md in the root directory
import mido import mido
from dataclasses import dataclass from dataclasses import dataclass
from enum import Enum
from pathlib import Path from pathlib import Path
from typing import BinaryIO, Optional, Dict, List from typing import BinaryIO, Optional, Dict, List, Callable, Tuple, Mapping
from Musicreater import SingleMusic from Musicreater import SingleMusic, SingleTrack, SingleNote, SoundAtmos
from Musicreater.plugins import ( from Musicreater.plugins import (
music_input_plugin, music_input_plugin,
PluginConfig, PluginConfig,
@@ -30,10 +31,8 @@ from Musicreater.plugins import (
PluginTypes, PluginTypes,
MusicInputPluginBase, MusicInputPluginBase,
) )
from Musicreater.types import ( from Musicreater.exceptions import ZeroSpeedError, IllegalMinimumVolumeError
FittingFunctionType, from Musicreater._utils import enumerated_stuffcopy_dictionary
)
from .constants import ( from .constants import (
MIDI_DEFAULT_PROGRAM_VALUE, MIDI_DEFAULT_PROGRAM_VALUE,
@@ -41,26 +40,134 @@ from .constants import (
MM_TOUCH_PERCUSSION_INSTRUMENT_TABLE, MM_TOUCH_PERCUSSION_INSTRUMENT_TABLE,
MM_TOUCH_PITCHED_INSTRUMENT_TABLE, MM_TOUCH_PITCHED_INSTRUMENT_TABLE,
) )
from .utils import velocity_2_distance_natural, panning_2_rotation_trigonometric from .exceptions import (
MidiFormatError,
NoteOnOffMismatchError,
ChannelOverFlowError,
LyricMismatchError,
)
from .utils import (
volume_2_distance_natural,
panning_2_rotation_trigonometric,
midi_msgs_to_noteinfo,
)
@dataclass @dataclass
class MidiImportConfig(PluginConfig): class MidiImportConfig(PluginConfig):
"""Midi 音乐数据导入插件配置""" """Midi 音乐数据导入插件配置"""
ignore_mismatch_error: bool = True # 系统设置
speed: float = 1.0 ignore_errors: bool = True
# 处理设置
speed_multiplier: float = 1.0
# 兼容不良 Midi 所定义的默认值
default_program_value: int = MIDI_DEFAULT_PROGRAM_VALUE default_program_value: int = MIDI_DEFAULT_PROGRAM_VALUE
default_volume_value: int = MIDI_DEFAULT_VOLUME_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: Dict[int, str] = MM_TOUCH_PITCHED_INSTRUMENT_TABLE
percussion_note_rtable: Dict[int, str] = MM_TOUCH_PERCUSSION_INSTRUMENT_TABLE # 对照表
vol_processing_function: FittingFunctionType = velocity_2_distance_natural pitched_note_reference_table: Mapping[int, str] = None # type: ignore
pan_processing_function: FittingFunctionType = panning_2_rotation_trigonometric percussion_note_reference_table: Mapping[int, str] = None # type: ignore
note_rtable_replacement: Dict[str, str] = {} note_replacement_table: Mapping[str, str] = None # type: ignore
# 参数转换函数
volume_process_function: Callable[[float], float] = volume_2_distance_natural
panning_processing_function: Callable[[float], float] = (
panning_2_rotation_trigonometric
)
# 分轨方式
divide_tracks_by_miditrack: bool = True
divide_tracks_by_midichannel: bool = False
divide_tracks_by_soundname: bool = True
divide_tracks_by_volume: bool = False
divide_tracks_by_panning: bool = False
def __post_init__(self):
self.pitched_note_reference_table = (
self.pitched_note_reference_table
if self.pitched_note_reference_table
else MM_TOUCH_PITCHED_INSTRUMENT_TABLE
)
self.percussion_note_reference_table = (
self.percussion_note_reference_table
if self.percussion_note_reference_table
else MM_TOUCH_PERCUSSION_INSTRUMENT_TABLE
)
self.note_replacement_table = (
self.note_replacement_table if self.note_replacement_table else {}
)
@music_input_plugin("midi_2_music_by_tracks") class ControlerKeys(Enum):
MIDI_PROGRAM = "midi_program"
MIDI_VOLUME = "midi_volume"
MIDI_PAN = "midi_pan"
class TrackDivisionDict(
Dict[
Tuple[
Optional[int],
Optional[int],
Optional[str],
Optional[float],
Optional[Tuple[float, float]],
],
SingleTrack,
]
):
"""
音轨分轨字典
"""
division_by_miditrack: bool = True
division_by_midichannel: bool = False
division_by_soundname: bool = True
division_by_volume: bool = False
division_by_panning: bool = False
def __init__(
self,
*args,
midi_import_config: MidiImportConfig = MidiImportConfig(),
**kwargs,
):
super().__init__(*args, **kwargs)
self.division_by_miditrack = midi_import_config.divide_tracks_by_miditrack
self.division_by_midichannel = midi_import_config.divide_tracks_by_midichannel
self.division_by_soundname = midi_import_config.divide_tracks_by_soundname
self.division_by_volume = midi_import_config.divide_tracks_by_volume
self.division_by_panning = midi_import_config.divide_tracks_by_panning
def __getitem__(
self,
key: Tuple[
Optional[int],
Optional[int],
Optional[str],
Optional[float],
Optional[Tuple[float, float]],
],
) -> SingleTrack:
key = (
key[0] if self.division_by_miditrack else None,
key[1] if self.division_by_midichannel else None,
key[2] if self.division_by_soundname else None,
key[3] if self.division_by_volume else None,
key[4] if self.division_by_panning else None,
)
try:
return super().__getitem__(key)
except KeyError:
self[key] = SingleTrack()
return self[key]
@music_input_plugin("midi_2_music_plugin")
class MidiImport2MusicPlugin(MusicInputPluginBase): class MidiImport2MusicPlugin(MusicInputPluginBase):
"""Midi 音乐数据导入插件""" """Midi 音乐数据导入插件"""
@@ -76,14 +183,305 @@ class MidiImport2MusicPlugin(MusicInputPluginBase):
supported_formats = ("MID", "MIDI") supported_formats = ("MID", "MIDI")
def loadbytes( def loadbytes(
self, bytes_buffer_in: BinaryIO, config: MidiImportConfig = MidiImportConfig() self,
bytes_buffer_in: BinaryIO,
config: Optional[MidiImportConfig] = MidiImportConfig(),
) -> SingleMusic: ) -> SingleMusic:
midi_file = mido.MidiFile(file=bytes_buffer_in) return self.midifile_2_singlemusic(
return SingleMusic() # =========================== TODO: 等待制作 mido.MidiFile(file=bytes_buffer_in),
config if config else MidiImportConfig(),
)
def load( def load(
self, file_path: Path, config: MidiImportConfig = MidiImportConfig() self, file_path: Path, config: Optional[MidiImportConfig] = MidiImportConfig()
) -> "SingleMusic": ) -> SingleMusic:
"""从 Midi 文件导入音乐数据""" """从 Midi 文件导入音乐数据"""
midi_file = mido.MidiFile(filename=file_path) return self.midifile_2_singlemusic(
return SingleMusic() # =========================== TODO: 等待制作 mido.MidiFile(filename=file_path), config if config else MidiImportConfig()
)
@staticmethod
def midifile_2_singlemusic(
midi: mido.MidiFile,
config: MidiImportConfig = MidiImportConfig(),
) -> SingleMusic:
"""
将midi解析并转换为频道音符字典
Parameters
----------
midi: mido.MidiFile 对象
需要处理的midi对象
speed: float
音乐播放速度倍数
default_program_value: int
默认的 MIDI 乐器值
default_volume_value: int
默认的通道音量值
default_tempo_value: int
默认的 MIDI TEMPO 值
pitched_note_rtable: Dict[int, Tuple[str, int]]
乐音乐器Midi-MC对照表
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 的替换
Returns
-------
Tuple[SingleMusic, int, Dict[str, int]]
以通道作为分割的Midi音符列表字典, 音符总数, 乐器使用统计
"""
if config.speed_multiplier == 0:
raise ZeroSpeedError("播放速度不得为零,应为 (0,1] 范围内的实数。")
# 一个midi中仅有16个通道 我们通过通道来识别而不是音轨
divided_tracks: TrackDivisionDict = TrackDivisionDict(midi_import_config=config)
value_controler_per_channel: Dict[int, Dict[ControlerKeys, int]] = (
enumerated_stuffcopy_dictionary(
staff={
ControlerKeys.MIDI_PROGRAM: config.default_program_value,
ControlerKeys.MIDI_VOLUME: config.default_volume_value,
ControlerKeys.MIDI_PAN: 64,
}
)
)
midi_tempo = config.default_tempo_value
"""微秒每拍"""
note_count = 0
"""音符计数"""
note_count_per_instrument: Dict[str, int] = {}
"""乐器使用统计"""
microseconds = 0
"""当前的微妙时间"""
note_queue_A: Dict[int, List[Tuple[int, int]]] = (
enumerated_stuffcopy_dictionary(staff=[])
)
"""音符队列甲 Dict[通道, List[Tuple[int音高, int乐器, int轨道]]]"""
note_queue_B: Dict[int, List[Tuple[int, int, int]]] = (
enumerated_stuffcopy_dictionary(staff=[])
)
"""音符队列乙 Dict[通道, List[Tuple[int音高, int微秒时间]]]"""
midi_lyric_cache: List[Tuple[int, str]] = []
"""歌词缓存 List[Tuple[int微秒时间, str歌词内容]]"""
midi_text_list: List[str] = []
"""Midi 附加文本列表"""
midi_copyright_list: List[str] = []
"""Midi 版权列表"""
midi_track_name_dict: Dict[int, str] = {}
"""轨道名称字典 Dict[int轨道编号, str轨道名称]"""
for track_no, message_track in enumerate(midi.tracks):
for msg in message_track:
if msg.type == "set_tempo":
midi_tempo = msg.tempo
if msg.time != 0:
# 微秒
# 通常情况下tempo 是 500000tpb 在
microseconds += msg.time * midi_tempo / midi.ticks_per_beat
if msg.type == "program_change":
# 检测 乐器变化 之 midi 事件
value_controler_per_channel[msg.channel][
ControlerKeys.MIDI_PROGRAM
] = msg.program
elif msg.is_cc(7):
# Control Change 更改当前通道的 音量 的事件(大幅度,最高有效位)
value_controler_per_channel[msg.channel][
ControlerKeys.MIDI_VOLUME
] = msg.value
elif msg.is_cc(10):
# Control Change 更改当前通道的 音调偏移 的事件(大幅度,最高有效位)
value_controler_per_channel[msg.channel][
ControlerKeys.MIDI_PAN
] = msg.value
elif msg.type == "lyrics":
# 歌词事件
midi_lyric_cache.append((microseconds, msg.text))
# print(lyric_cache, flush=True)
elif msg.type == "text":
# 检测文本事件
midi_text_list.append(msg.text)
elif msg.type == "copyright":
# 检测版权事件
midi_copyright_list.append(msg.text)
elif msg.type == "track_name":
# 检测轨道名称事件
midi_track_name_dict[track_no] = msg.name
elif msg.type == "note_on" and msg.velocity != 0:
# 一个音符开始弹奏
# 加入音符队列甲(按通道分隔)
# (音高, 轨道)
note_queue_A[msg.channel].append((msg.note, track_no))
# 音符队列乙(按通道分隔)
# (乐器, 力度, 微秒)
note_queue_B[msg.channel].append(
(
value_controler_per_channel[msg.channel][
ControlerKeys.MIDI_PROGRAM
],
msg.velocity,
microseconds,
)
)
elif (msg.type == "note_off") or (
msg.type == "note_on" and msg.velocity == 0
):
# 一个音符结束弹奏
if (
msg.note,
value_controler_per_channel[msg.channel][
ControlerKeys.MIDI_PROGRAM
],
) in note_queue_A[msg.channel]:
# 在甲队列中发现了同一个 音高和乐器且在同轨道 的音符
# 获取其音符力度和微秒数
_velocity, _program, _ms = note_queue_B[msg.channel][
note_queue_A[msg.channel].index((msg.note, track_no))
]
# 在队列中删除此音符
note_queue_A[msg.channel].remove((msg.note, track_no))
note_queue_B[msg.channel].remove((_velocity, _program, _ms))
_lyric = ""
# 找一找歌词吧
if midi_lyric_cache:
for i in range(len(midi_lyric_cache)):
if midi_lyric_cache[i][0] >= _ms:
_lyric = midi_lyric_cache.pop(i)[1]
break
# 更新结果信息
that_note, sound_name, orign_distance, sound_rotation = (
midi_msgs_to_noteinfo(
inst=(
msg.note
if (_is_percussion := (msg.channel == 9))
else _program
),
note=(_program if _is_percussion else msg.note),
percussive=_is_percussion,
volume=value_controler_per_channel[msg.channel][
ControlerKeys.MIDI_VOLUME
],
velocity=_velocity,
panning=value_controler_per_channel[msg.channel][
ControlerKeys.MIDI_PAN
],
start_time=_ms, # 微秒
duration=microseconds - _ms, # 微秒
play_speed=config.speed_multiplier,
midi_reference_table=(
config.percussion_note_reference_table
if _is_percussion
else config.pitched_note_reference_table
),
volume_processing_method=config.volume_process_function,
panning_processing_method=config.panning_processing_function,
note_table_replacement=config.note_replacement_table,
lyric_line=_lyric,
)
)
divided_tracks[
(
track_no,
msg.channel,
sound_name,
orign_distance,
sound_rotation,
)
].add(that_note)
# 更新统计信息
note_count += 1
if sound_name in note_count_per_instrument.keys():
note_count_per_instrument[sound_name] += 1
else:
note_count_per_instrument[sound_name] = 1
else:
# 什么?找不到 note on 消息??
if config.ignore_errors:
print(
"[WARRING] MIDI格式错误 音符不匹配`{}`无法在上文`{}`中找到与之匹配的音符开音消息".format(
msg, note_queue_A[msg.channel]
)
)
else:
raise NoteOnOffMismatchError(
"当前的MIDI很可能有损坏之嫌……",
msg,
"无法在上文中找到与之匹配的音符开音消息。",
)
"""整合后的音乐通道格式
每个通道包括若干消息元素其中逃不过这三种:
1 切换乐器消息
("PgmC", 切换后的乐器ID: int, 距离演奏开始的毫秒)
2 音符开始消息
("NoteS", 开始的音符ID, 力度(响度), 距离演奏开始的毫秒)
3 音符结束消息
("NoteE", 结束的音符ID, 距离演奏开始的毫秒)"""
del midi_tempo
if midi_lyric_cache:
# 怎么有歌词多啊
if config.ignore_errors:
print(
"[WARRING] MIDI 解析错误 歌词对应错误,以下歌词未能填入音符之中,已经填入的仍可能有误 {}".format(
midi_lyric_cache
)
)
else:
raise LyricMismatchError(
"MIDI 解析产生错误",
"歌词解析过程中无法对应音符,已填入的音符仍可能有误",
midi_lyric_cache,
)
final_music = SingleMusic(
credits="; ".join(midi_copyright_list),
extra_information={
"MIDI_TEXT_LIST": midi_text_list,
"NOTE_COUNT": note_count,
"NOTE_COUNT_PER_INSTRUMENT": note_count_per_instrument,
},
)
for track_properties, every_single_track in divided_tracks.items():
if track_properties[0] and (
track_name := midi_track_name_dict.get(track_properties[0])
):
every_single_track.track_name = track_name
if track_properties[2]:
every_single_track.track_instrument = track_properties[2]
if track_properties[3]:
every_single_track.sound_position.sound_distance = track_properties[3]
if track_properties[4]:
every_single_track.sound_position.sound_azimuth = track_properties[4]
final_music.append(every_single_track)
return final_music

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
@@ -20,18 +19,21 @@ Terms & Conditions: License.md in the root directory
import math import math
from typing import Callable, Dict, List, Optional, Sequence, Tuple, Mapping
from Musicreater import SingleNote, SoundAtmos
def velocity_2_distance_natural( def volume_2_distance_natural(
vol: float, vol: float,
) -> float: ) -> float:
""" """
midi力度值拟合成的距离函数 Midi 力度值/音量值拟合成的距离函数,一种更加自然的听感?
Parameters Parameters
---------- ----------
vol: int vol: int
midi 音符力度值 Midi 音符力度值0~127
Returns Returns
------- -------
@@ -50,20 +52,20 @@ def velocity_2_distance_natural(
) )
def velocity_2_distance_straight(vol: float) -> float: def volume_2_distance_straight(vol: float) -> float:
""" """
midi力度值拟合成的距离函数 Midi 力度值/音量值拟合成的距离函数,线性转换
Parameters Parameters
---------- ----------
vol: int vol: int
midi 音符力度值 Midi 音符力度值0~127
Returns Returns
------- -------
float播放中心到玩家的距离 float播放中心到玩家的距离
""" """
return vol / -8 + 16 return (vol + 1) / -8 + 16
def panning_2_rotation_linear(pan_: float) -> float: def panning_2_rotation_linear(pan_: float) -> float:
@@ -106,3 +108,115 @@ def panning_2_rotation_trigonometric(pan_: float) -> float:
else: else:
return math.degrees(math.acos((64 - pan_) / 63)) - 90 return math.degrees(math.acos((64 - pan_) / 63)) - 90
def midi_inst_to_mc_sound(
instrumentID: int,
reference_table: Mapping[int, str],
default_instrument: str = "note.flute",
) -> str:
"""
返回midi的乐器ID对应的我的世界乐器名
Parameters
----------
instrumentID: int
midi的乐器ID
reference_table: Dict[int, Tuple[str, int]]
转换乐器参照表
default_instrument: str
查无此乐器时的替换乐器
Returns
-------
str我的世界乐器名
"""
return reference_table.get(
instrumentID,
default_instrument,
)
def midi_msgs_to_noteinfo(
inst: int, # 乐器编号
note: int,
percussive: bool, # 是否作为打击乐器启用
volume: int,
velocity: int,
panning: int,
start_time: int,
duration: int,
play_speed: float,
midi_reference_table: Mapping[int, str],
volume_processing_method: Callable[[float], float],
panning_processing_method: Callable[[float], float],
note_table_replacement: Mapping[str, str] = {},
lyric_line: str = "",
) -> Tuple[SingleNote, str, float, Tuple[float, float]]:
"""
将 Midi信息转为音符对象
Parameters
------------
inst: int
乐器编号
note: int
音高编号(音符编号)
percussive: bool
是否作为打击乐器启用
volume: int
音量
velocity: int
力度
panning: int
声相偏移
start_time: int
音符起始时间(微秒)
duration: int
音符持续时间(微秒)
play_speed: float
曲目播放速度
midi_reference_table: Dict[int, str]
转换对照表
volume_processing_method: Callable[[float], float]
音量处理函数
panning_processing_method: Callable[[float], float]
立体声相偏移处理函数
note_table_replacement: Dict[str, str]
音符替换表,定义 Minecraft 音符字串的替换
lyric_line: str
该音符的歌词
Returns
---------
Tuple[
MineNote我的世界音符对象,
str我的世界声音名,
float播放中心到玩家的距离,
Tuple[float, float]声源旋转角度
]
"""
mc_sound_ID = midi_inst_to_mc_sound(
inst,
midi_reference_table,
"note.bd" if percussive else "note.flute",
)
return (
SingleNote(
midi_pitch=note,
note_volume=int((velocity / 127) + 0.5),
start_tick=(tk := int(start_time / float(play_speed) / 50000)),
keep_tick=round(duration / float(play_speed) / 50000),
mass_precision_time=round(
(start_time / float(play_speed) - tk * 50000) / 800
),
extra_information={
"LYRIC_TEXT": lyric_line,
"VOLUME_VALUE": volume,
"PIN_VALUE": panning,
},
),
note_table_replacement.get(mc_sound_ID, mc_sound_ID),
volume_processing_method(volume),
(panning_processing_method(panning), 0),
)

View File

@@ -34,15 +34,6 @@ z = "z"
z z
""" """
MIDI_PROGRAM = "program"
"""Midi乐器编号"""
MIDI_VOLUME = "volume"
"""Midi通道音量"""
MIDI_PAN = "pan"
"""Midi通道立体声场偏移"""
# Midi用对照表 # Midi用对照表

View File

@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
存储 音·创 v3 的内部数据类 音·创 v3 的内部数据类
""" """
""" """
@@ -41,6 +41,7 @@ from typing import (
Literal, Literal,
Hashable, Hashable,
TypeVar, TypeVar,
Mapping,
) )
from enum import Enum from enum import Enum
@@ -71,12 +72,14 @@ class SoundAtmos:
------------ ------------
distance: float distance: float
发声源距离玩家的距离(半径 `r` 发声源距离玩家的距离(半径 `r`
注:距离越近,音量越高,默认为 0。此参数可以与音量成某种函数关系 注:距离越近,音量越高,默认为 0。此参数可以作为音轨的音量使用
音量若默认为 +0则此值当为 8此值最小为 0.01,最大为 16。
azimuth: tuple[float, float] azimuth: tuple[float, float]
声源方位 声源方位
此参数为tuple包含两个元素分别表示 此参数为tuple包含两个元素分别表示
`rV` 发声源在竖直(上下)轴上,从玩家视角正前方开始,向顺时针旋转的角度 `rV` 发声源在竖直(上下)轴上,从玩家视角正前方开始,向顺时针旋转的角度
`rH` 发声源在水平(左右)轴上,从玩家视角正前方开始,向上(到达玩家正上方顶点后变为向下,以此类推的旋转)旋转的角度 `rH` 发声源在水平(左右)轴上,从玩家视角正前方开始,向上
(到达玩家正上方顶点后变为向下,以此类推的旋转)旋转的角度
""" """
self.sound_azimuth = (azimuth[0] % 360, azimuth[1] % 360) if azimuth else (0, 0) self.sound_azimuth = (azimuth[0] % 360, azimuth[1] % 360) if azimuth else (0, 0)
@@ -131,8 +134,8 @@ class SingleNote:
note_pitch: int note_pitch: int
"""midi音高""" """midi音高"""
velocity: int volume: int
"""力度""" """力度/播放响度 0~100 百分比"""
start_time: int start_time: int
"""开始之时 命令刻""" """开始之时 命令刻"""
@@ -149,9 +152,9 @@ class SingleNote:
def __init__( def __init__(
self, self,
midi_pitch: Optional[int], midi_pitch: Optional[int],
midi_velocity: int, note_volume: int,
start_time: int, start_tick: int,
last_time: int, keep_tick: int,
mass_precision_time: int = 0, mass_precision_time: int = 0,
extra_information: Dict[str, Any] = {}, extra_information: Dict[str, Any] = {},
): ):
@@ -162,8 +165,8 @@ class SingleNote:
------------ ------------
midi_pitch: int midi_pitch: int
midi音高 midi音高
midi_velocity: int note_volume: int
midi响度(力度) 响度/力度(百分比, 0~100)
start_time: int start_time: int
开始之时(命令刻) 开始之时(命令刻)
注:此处的时间是用从乐曲开始到当前的刻数 注:此处的时间是用从乐曲开始到当前的刻数
@@ -183,11 +186,11 @@ class SingleNote:
self.note_pitch: int = 66 if midi_pitch is None else midi_pitch self.note_pitch: int = 66 if midi_pitch is None else midi_pitch
"""midi音高""" """midi音高"""
self.velocity: int = midi_velocity self.volume: int = note_volume
"""响度(力度)""" """响度(力度)"""
self.start_time: int = start_time self.start_time: int = start_tick
"""开始之时 命令刻""" """开始之时 命令刻"""
self.duration: int = last_time self.duration: int = keep_tick
"""音符持续时间 命令刻""" """音符持续时间 命令刻"""
self.high_precision_start_time: int = mass_precision_time self.high_precision_start_time: int = mass_precision_time
"""高精度开始时间偏量 0.4 毫秒""" """高精度开始时间偏量 0.4 毫秒"""
@@ -201,15 +204,15 @@ class SingleNote:
group_1 := int.from_bytes(code_buffer[:6], "big") group_1 := int.from_bytes(code_buffer[:6], "big")
) & 0b11111111111111111 ) & 0b11111111111111111
start_tick_ = (group_1 := group_1 >> 17) & 0b11111111111111111 start_tick_ = (group_1 := group_1 >> 17) & 0b11111111111111111
note_velocity_ = (group_1 := group_1 >> 17) & 0b1111111 note_volume_ = (group_1 := group_1 >> 17) & 0b1111111
note_pitch_ = (group_1 := group_1 >> 7) & 0b1111111 note_pitch_ = (group_1 := group_1 >> 7) & 0b1111111
try: try:
return cls( return cls(
midi_pitch=note_pitch_, midi_pitch=note_pitch_,
midi_velocity=note_velocity_, note_volume=note_volume_,
start_time=start_tick_, start_tick=start_tick_,
last_time=duration_, keep_tick=duration_,
mass_precision_time=code_buffer[6] if is_high_time_precision else 0, mass_precision_time=code_buffer[6] if is_high_time_precision else 0,
) )
except Exception as e: except Exception as e:
@@ -245,7 +248,7 @@ class SingleNote:
# SingleNote 的字节码 # SingleNote 的字节码
# note_pitch 7 位 支持到 127 # note_pitch 7 位 支持到 127
# velocity 长度 7 位 支持到 127 # volume 长度 7 位 支持到 127
# start_tick 17 位 支持到 131071 即 109.22583 分钟 合 1.8204305 小时 # start_tick 17 位 支持到 131071 即 109.22583 分钟 合 1.8204305 小时
# duration 17 位 支持到 131071 即 109.22583 分钟 合 1.8204305 小时 # duration 17 位 支持到 131071 即 109.22583 分钟 合 1.8204305 小时
# 共 48 位 合 6 字节 # 共 48 位 合 6 字节
@@ -255,7 +258,7 @@ class SingleNote:
return ( return (
( (
( (
((((self.note_pitch << 7) + self.velocity) << 17) + self.start_time) ((((self.note_pitch << 7) + self.volume) << 17) + self.start_time)
<< 17 << 17
) )
+ self.duration + self.duration
@@ -290,9 +293,9 @@ class SingleNote:
return self.extra_info.get(key, default) return self.extra_info.get(key, default)
def stringize(self, include_extra_data: bool = False) -> str: def stringize(self, include_extra_data: bool = False) -> str:
return "TrackedNote(Pitch = {}, Velocity = {}, StartTick = {}, Duration = {}, TimeOffset = {}".format( return "TrackedNote(Pitch = {}, Volume = {}, StartTick = {}, Duration = {}, TimeOffset = {}".format(
self.note_pitch, self.note_pitch,
self.velocity, self.volume,
self.start_time, self.start_time,
self.duration, self.duration,
self.high_precision_start_time, self.high_precision_start_time,
@@ -308,7 +311,7 @@ class SingleNote:
) -> Tuple[int, int, int, int, int]: ) -> Tuple[int, int, int, int, int]:
return ( return (
self.note_pitch, self.note_pitch,
self.velocity, self.volume,
self.start_time, self.start_time,
self.duration, self.duration,
self.high_precision_start_time, self.high_precision_start_time,
@@ -317,39 +320,38 @@ class SingleNote:
def __dict__(self): def __dict__(self):
return { return {
"Pitch": self.note_pitch, "Pitch": self.note_pitch,
"Velocity": self.velocity, "Volume": self.volume,
"StartTick": self.start_time, "StartTick": self.start_time,
"Duration": self.duration, "Duration": self.duration,
"TimeOffset": self.high_precision_start_time, "TimeOffset": self.high_precision_start_time,
"ExtraData": self.extra_info, "ExtraData": self.extra_info,
} }
def __eq__(self, other) -> bool: def __eq__(self, other: "SingleNote") -> bool:
"""比较两个音符是否具有相同的属性,不计附加信息""" """比较两个音符是否具有相同的属性,不计附加信息"""
if not isinstance(other, self.__class__): if not isinstance(other, self.__class__):
return False return False
return self.__tuple__() == other.__tuple__() return self.__tuple__() == other.__tuple__()
def __lt__(self, other) -> bool: def __lt__(self, other: "SingleNote") -> bool:
"""比较自己是否在开始时间上早于另一个音符""" """比较自己是否在开始时间上早于另一个音符"""
if self.start_time == other.start_tick: if self.start_time == other.start_time:
return self.high_precision_start_time < other.high_precision_time return self.high_precision_start_time < other.high_precision_start_time
else: else:
return self.start_time < other.start_tick return self.start_time < other.start_time
def __gt__(self, other) -> bool: def __gt__(self, other: "SingleNote") -> bool:
"""比较自己是否在开始时间上晚于另一个音符""" """比较自己是否在开始时间上晚于另一个音符"""
if self.start_time == other.start_tick: if self.start_time == other.start_time:
return self.high_precision_start_time > other.high_precision_time return self.high_precision_start_time > other.high_precision_start_time
else: else:
return self.start_time > other.start_tick return self.start_time > other.start_time
class CurvableParam(str, Enum): class CurvableParam(str, Enum):
"""可曲线化的参数枚举类""" """可曲线化的参数枚举类"""
PITCH = "adjust_note_pitch" PITCH = "adjust_note_pitch"
VELOCITY = "adjust_note_velocity"
VOLUME = "adjust_note_volume" VOLUME = "adjust_note_volume"
DISTANCE = "adjust_note_sound_distance" DISTANCE = "adjust_note_sound_distance"
LR_PANNING = "adjust_note_leftright_panning_degree" LR_PANNING = "adjust_note_leftright_panning_degree"
@@ -361,13 +363,11 @@ class MineNote:
"""我的世界音符对象(仅提供我的世界相关接口)""" """我的世界音符对象(仅提供我的世界相关接口)"""
pitch: float pitch: float
"""midi音高""" """Midi 音高"""
instrument: str instrument: str
"""乐器ID""" """乐器 ID"""
velocity: float
"""力度"""
volume: float volume: float
"""音量""" """力度/播放音量 0~100 百分比"""
start_tick: int start_tick: int
"""开始之时 命令刻""" """开始之时 命令刻"""
duration_tick: int duration_tick: int
@@ -384,12 +384,10 @@ class MineNote:
cls, cls,
note: SingleNote, note: SingleNote,
note_instrument: str, note_instrument: str,
sound_volume: float,
is_persiced_time: bool, is_persiced_time: bool,
is_percussive_note: bool, is_percussive_note: bool,
sound_position: SoundAtmos, sound_position: SoundAtmos,
adjust_note_pitch: float = 0.0, adjust_note_pitch: float = 0.0,
adjust_note_velocity: float = 0.0,
adjust_note_volume: float = 0.0, adjust_note_volume: float = 0.0,
adjust_note_sound_distance: float = 0.0, adjust_note_sound_distance: float = 0.0,
adjust_note_leftright_panning_degree: float = 0.0, adjust_note_leftright_panning_degree: float = 0.0,
@@ -404,8 +402,7 @@ class MineNote:
return cls( return cls(
pitch=note.note_pitch + adjust_note_pitch, pitch=note.note_pitch + adjust_note_pitch,
instrument=note_instrument, instrument=note_instrument,
velocity=note.velocity + adjust_note_velocity, volume=note.volume + adjust_note_volume,
volume=sound_volume + adjust_note_volume,
start_tick=note.start_time, start_tick=note.start_time,
duration_tick=note.duration, duration_tick=note.duration,
persiced_time=note.high_precision_start_time if is_persiced_time else 0, persiced_time=note.high_precision_start_time if is_persiced_time else 0,
@@ -426,9 +423,6 @@ class SingleTrack(List[SingleNote]):
track_instrument: str track_instrument: str
"""乐器ID""" """乐器ID"""
track_volume: float
"""该音轨的音量"""
is_high_time_precision: bool is_high_time_precision: bool
"""该音轨是否使用高精度时间""" """该音轨是否使用高精度时间"""
@@ -446,14 +440,13 @@ class SingleTrack(List[SingleNote]):
def __init__( def __init__(
self, self,
*args: SingleNote,
name: str = "未命名音轨", name: str = "未命名音轨",
instrument: str = "", instrument: str = "",
volume: float = 0,
precise_time: bool = True, precise_time: bool = True,
percussion: bool = False, percussion: bool = False,
sound_direction: SoundAtmos = SoundAtmos(), sound_direction: SoundAtmos = SoundAtmos(),
extra_information: Dict[str, Any] = {}, extra_information: Dict[str, Any] = {},
*args: SingleNote,
): ):
self.track_name = name self.track_name = name
"""音轨名称""" """音轨名称"""
@@ -461,9 +454,6 @@ class SingleTrack(List[SingleNote]):
self.track_instrument = instrument self.track_instrument = instrument
"""乐器ID""" """乐器ID"""
self.track_volume = volume
"""音量"""
self.is_high_time_precision = precise_time self.is_high_time_precision = precise_time
"""是否使用高精度时间""" """是否使用高精度时间"""
@@ -559,7 +549,6 @@ class SingleTrack(List[SingleNote]):
yield MineNote.from_single_note( yield MineNote.from_single_note(
note=_note, note=_note,
note_instrument=self.track_instrument, note_instrument=self.track_instrument,
sound_volume=self.track_volume,
is_persiced_time=self.is_high_time_precision, is_persiced_time=self.is_high_time_precision,
is_percussive_note=self.is_percussive, is_percussive_note=self.is_percussive,
sound_position=self.sound_position, sound_position=self.sound_position,
@@ -626,12 +615,12 @@ class SingleMusic(List[SingleTrack]):
def __init__( def __init__(
self, self,
*args: SingleTrack,
name: str = "未命名乐曲", name: str = "未命名乐曲",
creator: str = "未命名制作者", creator: str = "未命名制作者",
original_author: str = "未命名原作者", original_author: str = "未命名原作者",
description: str = "未命名简介", description: str = "未命名简介",
credits: str = "未命名版权信息", credits: str = "未命名版权信息",
*args: SingleTrack,
extra_information: Dict[str, Any] = {}, extra_information: Dict[str, Any] = {},
): ):
self.music_name = name self.music_name = name

View File

@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
存储 音·创 v3 用到的一些报错类型 音·创 v3 用到的一些报错类型
""" """
""" """
@@ -106,10 +106,10 @@ class OuterlyParameterError(MusicreaterOuterlyError):
class ZeroSpeedError(OuterlyParameterError, ZeroDivisionError): class ZeroSpeedError(OuterlyParameterError, ZeroDivisionError):
"""0作为播放速度的错误""" """ 0 作为播放速度的错误"""
def __init__(self, *args): def __init__(self, *args):
"""0作为播放速度的错误""" """ 0 作为播放速度的错误"""
super().__init__("播放速度为零:", *args) super().__init__("播放速度为零:", *args)

View File

@@ -41,7 +41,7 @@ https://gitee.com/TriM-Organization/Musicreater/blob/master/LICENSE.md。
# Bug retreat! Bug retreat! # Bug retreat! Bug retreat!
# Exceptions and errors are causing chaos # Exceptions and errors are causing chaos
# Words combine! Codes unite! # Words combine! Codes unite!
# Hurry to call the programmer! Let's Go! # Hurry to call the Programmer! Let's Go!
import re import re
@@ -96,9 +96,10 @@ class MusiCreater:
def _get_plugin_within_iousage( def _get_plugin_within_iousage(
get_func: Callable[[Union[Path, str]], Generator[T_IOPlugin, None, None]], get_func: Callable[[Union[Path, str]], Generator[T_IOPlugin, None, None]],
fpath: Path, fpath: Path,
plg_regdict: Dict[str, T_IOPlugin], plg_regdict: Mapping[str, T_IOPlugin],
plg_id: Optional[str], plg_id: Optional[str],
) -> T_IOPlugin: ) -> T_IOPlugin:
"""这个函数是用于从指定的注册表项里面调取实例的,仅供下面这几个函数使用"""
__plugin: Optional[T_IOPlugin] = None __plugin: Optional[T_IOPlugin] = None
if plg_id: if plg_id:

View File

@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
存储 音·创 v3 内部数据使用的参数曲线 音·创 v3 内部数据使用的参数曲线
""" """
""" """
@@ -26,12 +26,10 @@ Terms & Conditions: License.md in the root directory
from math import ceil from math import ceil
from dataclasses import dataclass from dataclasses import dataclass
from typing import Optional, Any, List, Tuple from typing import Optional, Any, List, Tuple, Callable
from enum import Enum from enum import Enum
import bisect import bisect
from .types import FittingFunctionType
def _evaluate_bezier_segment( def _evaluate_bezier_segment(
t0: float, t0: float,
@@ -178,7 +176,7 @@ class Keyframe:
value: float value: float
# 函数插值模式 # 函数插值模式
out_interp: Optional[FittingFunctionType] = None out_interp: Optional[Callable[[float], float]] = None
# 贝塞尔模式 # 贝塞尔模式
in_tangent: Optional[Tuple[float, float]] = ( in_tangent: Optional[Tuple[float, float]] = (
@@ -215,7 +213,7 @@ class ParamCurve:
base_line: float = 0.0 base_line: float = 0.0
"""基线/默认值""" """基线/默认值"""
base_interpolation_function: FittingFunctionType base_interpolation_function: Callable[[float], float]
"""默认(未指定区间时的)关键帧插值模式""" """默认(未指定区间时的)关键帧插值模式"""
boundary_behaviour: BoundaryBehaviour boundary_behaviour: BoundaryBehaviour
@@ -227,7 +225,7 @@ class ParamCurve:
def __init__( def __init__(
self, self,
base_value: float = 0.0, base_value: float = 0.0,
default_interpolation_function: FittingFunctionType = InterpolationMethod.linear, default_interpolation_function: Callable[[float], float] = InterpolationMethod.linear,
boundary_mode: BoundaryBehaviour = BoundaryBehaviour.CONSTANT, boundary_mode: BoundaryBehaviour = BoundaryBehaviour.CONSTANT,
): ):
""" """
@@ -257,7 +255,7 @@ class ParamCurve:
self, self,
time: float, time: float,
value: float, value: float,
out_interp: Optional[FittingFunctionType] = None, out_interp: Optional[Callable[[float], float]] = None,
in_tangent: Optional[Tuple[float, float]] = None, in_tangent: Optional[Tuple[float, float]] = None,
out_tangent: Optional[Tuple[float, float]] = None, out_tangent: Optional[Tuple[float, float]] = None,
use_bezier: bool = False, use_bezier: bool = False,
@@ -328,7 +326,7 @@ class ParamCurve:
def update_key_interp( def update_key_interp(
self, self,
time: float, time: float,
out_interp: Optional[FittingFunctionType] = None, out_interp: Optional[Callable[[float], float]] = None,
in_tangent: Optional[Tuple[float, float]] = None, in_tangent: Optional[Tuple[float, float]] = None,
out_tangent: Optional[Tuple[float, float]] = None, out_tangent: Optional[Tuple[float, float]] = None,
use_bezier: bool = False, use_bezier: bool = False,
@@ -486,7 +484,7 @@ class ParamCurve:
"""返回 (time, value) 列表。""" """返回 (time, value) 列表。"""
return [(k.time, k.value) for k in self._keys] return [(k.time, k.value) for k in self._keys]
def set_default_interpolation_function(self, interp_func: FittingFunctionType): def set_default_interpolation_function(self, interp_func: Callable[[float], float]):
"""设置默认插值函数。""" """设置默认插值函数。"""
self.base_interpolation_function = interp_func self.base_interpolation_function = interp_func

View File

@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
存储 音·创 v3 的插件接口与管理相关内容 音·创 v3 的插件接口与管理相关内容
""" """
""" """
@@ -213,7 +213,7 @@ class PluginRegistry:
@staticmethod @staticmethod
def _get_io_plugin_by_format( def _get_io_plugin_by_format(
plugin_regdict: Dict[str, T_IOPlugin], fpath_or_format: Union[Path, str] plugin_regdict: Mapping[str, T_IOPlugin], fpath_or_format: Union[Path, str]
) -> Generator[T_IOPlugin, None, None]: ) -> Generator[T_IOPlugin, None, None]:
if isinstance(fpath_or_format, str): if isinstance(fpath_or_format, str):
return ( return (

View File

@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
存储 音·创 v3 定义的一些数据类型,可以用于类型检查器 音·创 v3 定义的一些数据类型,可以用于类型检查器
""" """
""" """
@@ -19,7 +19,3 @@ Terms & Conditions: License.md in the root directory
from typing import Callable, Dict, List, Literal, Mapping, Tuple, Union from typing import Callable, Dict, List, Literal, Mapping, Tuple, Union
FittingFunctionType = Callable[[float], float]
"""
拟合函数类型
"""

View File

@@ -6,31 +6,31 @@
1. 使用 `.MCT` 作为项目文件的后缀然后考虑一下格式是否和之前的 MusicSequence 兼容如果兼容的话可以照旧用 `.MSQ`如不的话可以试试想一个新的后缀名作为数据文件后缀 1. 使用 `.MCT` 作为项目文件的后缀然后考虑一下格式是否和之前的 MusicSequence 兼容如果兼容的话可以照旧用 `.MSQ`如不的话可以试试想一个新的后缀名作为数据文件后缀
2. 要求数据文件支持完全流式读入 2. 要求数据文件支持完全流式读入
- 音轨静音处理 - [] 音轨静音处理
当前没有处理 当前没有处理
- 优化音轨的存储方式 - [] 优化音轨的存储方式
当前是用列表且每一次变动元素都要重新排序这样消耗太大了需要优化改用最小堆形式heapq 当前是用列表且每一次变动元素都要重新排序这样消耗太大了需要优化改用最小堆形式heapq
- 移植 v2 功能到内置插件 - 移植 v2 功能到内置插件
目前 v2 的功能有很多都要移植到 v3 目前 v2 的功能有很多都要移植到 v3
1. 导入 Midi 文件到全曲 1. [x] 导入 Midi 文件到全曲
2. 导入 Midi 文件到指定轨道 2. [] 导入 Midi 文件到指定轨道
3. 导出到延迟播放器的结构文件MCSTRUCTUREBDX 3. [] 导出到延迟播放器的结构文件MCSTRUCTUREBDX
4. 导出到延迟播放器的附加包 4. [] 导出到延迟播放器的附加包
5. 导出到积分板播放器的以上两种形式 5. [] 导出到积分板播放器的以上两种形式
6. 导出到中继器播放器的以上两种形式 6. [] 导出到中继器播放器的以上两种形式
7. WebSocket 播放器中播放 7. [] WebSocket 播放器中播放
8. 导出到支持神羽资源包的以上 7 种形式 8. [] 导出到支持神羽资源包的以上 7 种形式
9. 对于 Midi 歌词的实验性功能 9. [] 对于 Midi 歌词的实验性功能
10. 对于 Java 版本适配的实验性功能 10. [] 对于 Java 版本适配的实验性功能
11. 对于听感优化的实验性功能插值偏移 11. [] 对于听感优化的实验性功能插值偏移
- 测试参数曲线的功能 - [] 测试参数曲线的功能
- 支持导出音符盒构成的音乐 - [] 支持导出音符盒构成的音乐
- 支持导出成 schematic 结构 - [] 支持导出成 schematic 结构
## 讨论 ## 讨论
@@ -41,8 +41,8 @@
引入了插件惟一识别码之后当然是采用 `Dict[插件唯一识别码, 插件对象]` 来存储插件了~之前插件名称的内容是我想得太浅了我写完所有代码之后才想到插件名称是中文还带空格的任意字符串 引入了插件惟一识别码之后当然是采用 `Dict[插件唯一识别码, 插件对象]` 来存储插件了~之前插件名称的内容是我想得太浅了我写完所有代码之后才想到插件名称是中文还带空格的任意字符串
2. 服务插件到底该怎么写总不能留着一个 PluginType.SERVICE 的插件一直空在那里吧 2. [] 服务插件到底该怎么写总不能留着一个 PluginType.SERVICE 的插件一直空在那里吧
3. 插件依赖性的优化目前没有处理各个插件依赖关系的问题如果插件之间彼此依赖要怎么做 3. [] 插件依赖性的优化目前没有处理各个插件依赖关系的问题如果插件之间彼此依赖要怎么做
我的想法是这个依赖的处理由调用端来完成比如我们的 伶伦工作站 是以 · 为核心的一个可视化数字音频工作站 我的想法是这个依赖的处理由调用端来完成比如我们的 伶伦工作站 是以 · 为核心的一个可视化数字音频工作站
那么应该由伶伦来处理依赖关系并加载之 那么应该由伶伦来处理依赖关系并加载之

237
docs/异常继承关系.mmd Normal file
View File

@@ -0,0 +1,237 @@
classDiagram
direction LR
class Exception{
Python 内置基类
}
class MusicreaterBaseException {
"[音·创] - ..."
所有音·创 v3 错误的基类
}
class MusicreaterInnerlyError {
"内部错误 - ..."
面向开发者的内部错误
}
class MusicreaterOuterlyError {
"外部错误 - ..."
面向用户的外部错误
}
class InnerlyParameterError {
"内部传参错误 - ..."
内部参数相关错误
}
class OuterlyParameterError {
"参数错误 - ..."
外部参数相关错误
}
class ParameterTypeError {
"参数类型错误:..."
继承自 InnerlyParameterError 和 TypeError
}
class ParameterValueError {
"参数数值错误:..."
继承自 InnerlyParameterError 和 ValueError
}
class PluginNotSpecifiedError {
"未指定插件:..."
继承自 InnerlyParameterError 和 LookupError
}
class ZeroSpeedError {
"播放速度为零:..."
继承自 OuterlyParameterError 和 ZeroDivisionError
}
class IllegalMinimumVolumeError {
"最小播放音量超出范围:..."
继承自 OuterlyParameterError 和 ValueError
}
class FileFormatNotSupportedError {
"不支持的文件格式:..."
继承自 MusicreaterOuterlyError
}
class NoteBinaryDecodeError {
"解码音乐存储二进制数据时出现问题 - ..."
继承自 MusicreaterOuterlyError
}
class SingleNoteDecodeError {
"音符解码出错:..."
继承自 NoteBinaryDecodeError
}
class NoteBinaryFileTypeError {
"无法识别音乐存储文件对应的类型:..."
继承自 NoteBinaryDecodeError
}
class NoteBinaryFileVerificationFailed {
"音乐存储文件校验失败:..."
继承自 NoteBinaryDecodeError
}
class PluginDefineError {
"插件内部错误 - ..."
插件定义相关的内部错误
}
class PluginInstanceNotFoundError {
"插件实例未找到:..."
继承自 PluginDefineError 和 LookupError
}
class PluginAttributeNotFoundError {
"插件类的必要属性不存在:..."
继承自 PluginDefineError 和 AttributeError
}
class PluginMetainfoError {
"插件元信息定义错误 - ..."
插件元信息相关错误
}
class PluginMetainfoTypeError {
"插件元信息类型错误:..."
继承自 PluginMetainfoError 和 TypeError
}
class PluginMetainfoValueError {
"插件元信息数值错误:..."
继承自 PluginMetainfoError 和 ValueError
}
class PluginMetainfoNotFoundError {
"插件元信息未定义:..."
继承自 PluginMetainfoError 和 PluginAttributeNotFoundError
}
class PluginLoadError {
"插件加载错误 - ..."
插件加载相关的外部错误
}
class PluginNotFoundError {
"插件未找到:..."
继承自 PluginLoadError
}
class PluginRegisteredError {
"插件重复注册:..."
继承自 PluginLoadError
}
class PluginConfigRelatedError {
"插件配置相关错误 - ..."
插件配置相关错误基类
}
class PluginConfigLoadError {
"插件配置文件加载错误:..."
继承自 PluginLoadError、PluginConfigRelatedError
}
class PluginConfigDumpError {
"插件配置文件保存错误:..."
继承自 PluginConfigRelatedError
}
%% 高亮定义
class ParameterTypeError ::: highlight
class ParameterValueError ::: highlight
class PluginNotSpecifiedError ::: highlight
class ZeroSpeedError ::: highlight
class IllegalMinimumVolumeError ::: highlight
class FileFormatNotSupportedError ::: highlight
class SingleNoteDecodeError ::: highlight
class NoteBinaryFileTypeError ::: highlight
class NoteBinaryFileVerificationFailed ::: highlight
class PluginInstanceNotFoundError ::: highlight
class PluginAttributeNotFoundError ::: highlight
class PluginMetainfoTypeError ::: highlight
class PluginMetainfoValueError ::: highlight
class PluginMetainfoNotFoundError ::: highlight
class PluginNotFoundError ::: highlight
class PluginRegisteredError ::: highlight
class PluginConfigLoadError ::: highlight
class PluginConfigDumpError ::: highlight
%% 定义高亮样式
classDef highlight fill:,stroke-width:5px
%% 继承关系(箭头从子类指向父类)
Exception <|-- MusicreaterBaseException
Exception <|-- TypeError
Exception <|-- ValueError
Exception <|-- LookupError
Exception <|-- AttributeError
Exception <|-- ZeroDivisionError
MusicreaterBaseException <|-- MusicreaterInnerlyError
MusicreaterBaseException <|-- MusicreaterOuterlyError
MusicreaterInnerlyError <|-- InnerlyParameterError
MusicreaterOuterlyError <|-- OuterlyParameterError
InnerlyParameterError <|-- ParameterTypeError
TypeError <|-- ParameterTypeError
InnerlyParameterError <|-- ParameterValueError
ValueError <|-- ParameterValueError
InnerlyParameterError <|-- PluginNotSpecifiedError
LookupError <|-- PluginNotSpecifiedError
OuterlyParameterError <|-- ZeroSpeedError
ZeroDivisionError <|-- ZeroSpeedError
OuterlyParameterError <|-- IllegalMinimumVolumeError
ValueError <|-- IllegalMinimumVolumeError
MusicreaterOuterlyError <|-- FileFormatNotSupportedError
MusicreaterOuterlyError <|-- NoteBinaryDecodeError
NoteBinaryDecodeError <|-- SingleNoteDecodeError
NoteBinaryDecodeError <|-- NoteBinaryFileTypeError
NoteBinaryDecodeError <|-- NoteBinaryFileVerificationFailed
MusicreaterInnerlyError <|-- PluginDefineError
PluginDefineError <|-- PluginInstanceNotFoundError
LookupError <|-- PluginInstanceNotFoundError
PluginDefineError <|-- PluginAttributeNotFoundError
AttributeError <|-- PluginAttributeNotFoundError
PluginDefineError <|-- PluginMetainfoError
PluginMetainfoError <|-- PluginMetainfoTypeError
TypeError <|-- PluginMetainfoTypeError
PluginMetainfoError <|-- PluginMetainfoValueError
ValueError <|-- PluginMetainfoValueError
PluginMetainfoError <|-- PluginMetainfoNotFoundError
PluginAttributeNotFoundError <|-- PluginMetainfoNotFoundError
MusicreaterOuterlyError <|-- PluginLoadError
PluginLoadError <|-- PluginNotFoundError
PluginLoadError <|-- PluginRegisteredError
MusicreaterOuterlyError <|-- PluginConfigRelatedError
PluginLoadError <|-- PluginConfigLoadError
PluginConfigRelatedError <|-- PluginConfigLoadError
PluginConfigRelatedError <|-- PluginConfigDumpError

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 652 KiB

View File

@@ -0,0 +1,125 @@
# 示例插件:导入音符数据
> 版权所有 © 2026 金羿
> Copyright © 2026 Eilles
睿乐组织 开发交流群 [861684859](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=fxNYIX_zKMgaO8X6K7pP7tHtLB7JRvdX&noverify=0&group_code=861684859)
Email [TriM-Organization@hotmail.com](mailto:TriM-Organization@hotmail.com)
```license
本示例模块开放授权同时本教程文件已开放至公共领域
请注意
若是对本文件的直接转载在形式上没有修改增删添加注释或单纯修改排版翻译录屏截图
则该使用者需要在转载所及之处明确在转载的内容开头标注本文之原始著作权人
在当前文件下该原始著作权人为金羿(Eilles)
如果是对本文进行了一定程度上的修改和补充或者以不同方式演绎本文件如制成视频教程等
则无需标注原作者允许该使用者自行署名
本声明仅限于包含此声明的本文件本声明与项目内其他文件无关
```
## 新建文件夹 · 基础模块知识
首先一个 **· v3** 的插件应当存储于一个 Python 模块之中也就是插件存在于可以被 import 语句引入的 module
这就意味着承载插件的模块本质上可以是多个 Python `.py` 文件组成的带有 `__init__.py` 的一个文件夹
或者是一个简单的 `.py` 文件
我们有这种共识你已经知道了模块的相关知识我后面无需赘述插件和模块的区别
## 开始编写插件 · 插件基础
首先导入插件所需的类
在这里我们是一个用来导入数据的插件
所以就需要导入 `MusicInputPluginBase` 类和 `music_input_plugin` 函数
同时`PluginMetaInformation` 类和 `PluginTypes` 类也必须导入这是插件的元信息所需要的
```python
from Musicreater.plugins import (
music_input_plugin,
PluginMetaInformation,
PluginTypes,
MusicInputPluginBase,
)
```
如果插件需要配置那么请再导入 `PluginConfig` 并从此继承一个类且须用 dataclass 装饰器来注册之
_对于这个类的使用方式可以阅读 dataclass 的官方文档_
```python
from Musicreater.plugins import PluginConfig
from dataclasses import dataclass
@dataclass
class ExampleImportConfig(PluginConfig):
example_config_item3: bool
example_config_item1: str = "example_config_item"
example_config_item2: int = 0
```
## 编写插件 · 开始
接着我们来制作一个插件
首先一个 **· v3** 的插件应当是一个继承自我们已经准备好的插件基类的****缩句插件是类
**· v3** 任何对音乐的操作包括导入导出处理都分为对 **整首曲目** 的操作和对 **单个音轨** 的操作
我们的样例是一个对**整首曲目**进行**导入操作**的插件因此需要继承 `MusicInputPluginBase`
插件类的类名称不得以 `Base` 结尾因为咱写的是插件不是插件基类
在插件的类的开头需要用插件注册装饰函数来对插件类装饰
```python
@music_input_plugin("example_import_plugin")
class xxx:
...
```
我们这里对应插件类型的注册器是 `music_input_plugin` 函数
在注册器函数后的参数是这个插件的惟一识别码不应与其他插件混淆
通常可以是这个插件的功能描述或者就是插件名
接着编写这个插件也即是此类
每个插件的类必须包含一个用于指定插件元信息的 `metainfo` 属性
如果插件是导入数据或者导出数据的插件则必须包含一个 `supported_formats` 属性用以声明插件所支持的数据格式
用于导入的插件类必须包含一个 `loadbytes` 方法用于从字节流中导入数据可选是否单独实现 `load` 方法如果不单独实现则在调用时会直接通过打开文件后使用 `loadbytes` 的方式实现
```python
# 注册插件
@music_input_plugin("something_convert_to_music")
# 继承自对应类型的插件基类
class ExampleImportPlugin(MusicInputPluginBase):
# 插件元信息定义
metainfo = PluginMetaInformation(
name="示例导入插件", # 插件名称
author="金羿", # 插件作者
description="这是一个示例导入插件", # 插件描述
version=(0, 0, 1), # 插件版本
type=PluginTypes.FUNCTION_MUSIC_IMPORT, # 插件类型
license="The Unlicense", # 插件许可证
dependencies=("something_convertion_library") # 插件对于其他插件的依赖项
)
# 导入导出插件支持的数据格式,大小写皆可
supported_formats = ("EXP", "example_format")
# 定义 loadbytes 方法,从字节流中导入数据
def loadbytes(
self, bytes_buffer_in: BinaryIO, config: ExampleImportConfig
) -> "SingleMusic":
...
# 插件可选地定义 load 方法,从文件导入数据
def load(
self, file_path: Path, config: ExampleImportConfig
) -> "SingleMusic":
...
```
至此一个插件的编写已经完成

View File

@@ -0,0 +1,70 @@
# -*- coding: utf-8 -*-
"""
示例插件:导入音符数据
"""
"""
版权所有 © 2026 金羿
Copyright © 2026 Eilles
"""
# 睿乐组织 开发交流群 861684859
# Email TriM-Organization@hotmail.com
"""
本示例模块开放授权,本文件已开放至公共领域。
请注意:
若是对本文件的直接转载(在形式上没有修改、增删、添加注释,或单纯修改排版、翻译、录屏、截图)
则该使用者需要在转载所及之处,明确在转载的内容开头标注本文之原始著作权人
在当前文件下,该原始著作权人为金羿(Eilles)
如果是对本文进行了一定程度上的修改和补充、或者以不同方式演绎本文件(如制成视频教程等)
则无需标注原作者,允许该使用者自行署名
本声明仅限于包含此声明的本文件,本声明与项目内其他文件无关。
"""
from typing import BinaryIO
from pathlib import Path
from dataclasses import dataclass
from Musicreater import SingleMusic
from Musicreater.plugins import (
music_input_plugin,
PluginConfig,
PluginMetaInformation,
PluginTypes,
MusicInputPluginBase,
)
@dataclass
class ExampleImportConfig(PluginConfig):
example_config_item3: bool
example_config_item1: str = "example_config_item"
example_config_item2: int = 0
@music_input_plugin("something_convert_to_music")
class ExampleImportPlugin(MusicInputPluginBase):
metainfo = PluginMetaInformation(
name="示例导入插件",
author="金羿",
description="这是一个示例导入插件",
version=(0, 0, 1),
type=PluginTypes.FUNCTION_MUSIC_IMPORT,
license="The Unlicense",
dependencies=("something_convertion_library")
)
supported_formats = ("EXP", "example_format")
def loadbytes(
self, bytes_buffer_in: BinaryIO, config: ExampleImportConfig
) -> "SingleMusic":
return SingleMusic()
def load(
self, file_path: Path, config: ExampleImportConfig
) -> "SingleMusic":
return SingleMusic()

View File

@@ -182,10 +182,10 @@ class FutureMidiConvertKamiRES(MidiConvert):
raise ZeroSpeedError("播放速度为 0 ,其需要(0,1]范围内的实数。") raise ZeroSpeedError("播放速度为 0 ,其需要(0,1]范围内的实数。")
# 一个midi中仅有16个通道 我们通过通道来识别而不是音轨 # 一个midi中仅有16个通道 我们通过通道来识别而不是音轨
midi_channels: MineNoteChannelType = empty_midi_channels(default_staff=[]) midi_channels: MineNoteChannelType = enumerated_stuff_copy(staff=[])
channel_controler: Dict[int, Dict[str, int]] = empty_midi_channels( channel_controler: Dict[int, Dict[str, int]] = enumerated_stuff_copy(
default_staff={ staff={
MIDI_PROGRAM: default_program_value, MIDI_PROGRAM: default_program_value,
MIDI_VOLUME: default_volume_value, MIDI_VOLUME: default_volume_value,
MIDI_PAN: 64, MIDI_PAN: 64,
@@ -205,7 +205,7 @@ class FutureMidiConvertKamiRES(MidiConvert):
int, int,
] ]
], ],
] = empty_midi_channels(default_staff=[]) ] = enumerated_stuff_copy(staff=[])
note_queue_B: Dict[ note_queue_B: Dict[
int, int,
List[ List[
@@ -214,7 +214,7 @@ class FutureMidiConvertKamiRES(MidiConvert):
int, int,
] ]
], ],
] = empty_midi_channels(default_staff=[]) ] = enumerated_stuff_copy(staff=[])
# 直接使用mido.midifiles.tracks.merge_tracks转为单轨 # 直接使用mido.midifiles.tracks.merge_tracks转为单轨
# 采用的时遍历信息思路 # 采用的时遍历信息思路
@@ -1042,7 +1042,7 @@ class FutureMidiConvertM5(MidiConvert):
# ) # )
# 一个midi中仅有16个通道 我们通过通道来识别而不是音轨 # 一个midi中仅有16个通道 我们通过通道来识别而不是音轨
midi_channels: ChannelType = empty_midi_channels() midi_channels: ChannelType = enumerated_stuff_copy()
tempo = 500000 tempo = 500000
# 我们来用通道统计音乐信息 # 我们来用通道统计音乐信息
@@ -1052,7 +1052,7 @@ class FutureMidiConvertM5(MidiConvert):
if not track: if not track:
continue continue
note_queue = empty_midi_channels(default_staff=[]) note_queue = enumerated_stuff_copy(staff=[])
for msg in track: for msg in track:
if msg.time != 0: if msg.time != 0:

View File

@@ -34,14 +34,6 @@ class MSCTBaseException(Exception):
raise self raise self
class MidiFormatException(MSCTBaseException):
"""音·创 的所有MIDI格式错误均继承于此"""
def __init__(self, *args):
"""音·创 的所有MIDI格式错误均继承于此"""
super().__init__("MIDI 格式错误", *args)
class MidiDestroyedError(MSCTBaseException): class MidiDestroyedError(MSCTBaseException):
"""Midi文件损坏""" """Midi文件损坏"""
@@ -82,84 +74,3 @@ class CommandFormatError(MSCTBaseException, RuntimeError):
# 那么这两个音符的音长无法判断。这是个好问题,但是不是我现在能解决的,也不是我们现在想解决的问题 # 那么这两个音符的音长无法判断。这是个好问题,但是不是我现在能解决的,也不是我们现在想解决的问题
class NotDefineTempoError(MidiFormatException):
"""没有Tempo设定导致时间无法计算的错误"""
def __init__(self, *args):
"""没有Tempo设定导致时间无法计算的错误"""
super().__init__("在曲目开始时没有声明 Tempo未指定拍长", *args)
class ChannelOverFlowError(MidiFormatException):
"""一个midi中含有过多的通道"""
def __init__(self, max_channel=16, *args):
"""一个midi中含有过多的通道"""
super().__init__("含有过多的通道(数量应≤{}".format(max_channel), *args)
class NotDefineProgramError(MidiFormatException):
"""没有Program设定导致没有乐器可以选择的错误"""
def __init__(self, *args):
"""没有Program设定导致没有乐器可以选择的错误"""
super().__init__("未指定演奏乐器", *args)
class NoteOnOffMismatchError(MidiFormatException):
"""音符开音和停止不匹配的错误"""
def __init__(self, *args):
"""音符开音和停止不匹配的错误"""
super().__init__("音符不匹配", *args)
class LyricMismatchError(MSCTBaseException):
"""歌词匹配解析错误"""
def __init__(self, *args):
"""有可能产生了错误的歌词解析"""
super().__init__("歌词解析错误", *args)
# 已重构
class ZeroSpeedError(MSCTBaseException, ZeroDivisionError):
"""以0作为播放速度的错误"""
def __init__(self, *args):
"""以0作为播放速度的错误"""
super().__init__("播放速度为零", *args)
# 已重构
class IllegalMinimumVolumeError(MSCTBaseException, ValueError):
"""最小播放音量有误的错误"""
def __init__(self, *args):
"""最小播放音量错误"""
super().__init__("最小播放音量超出范围", *args)
# 已重构
class MusicSequenceDecodeError(MSCTBaseException):
"""音乐序列解码错误"""
def __init__(self, *args):
"""音乐序列无法正确解码的错误"""
super().__init__("解码音符序列文件时出现问题", *args)
# 已重构
class MusicSequenceTypeError(MSCTBaseException):
"""音乐序列类型错误"""
def __init__(self, *args):
"""无法识别音符序列字节码的类型"""
super().__init__("错误的音符序列字节类型", *args)
# 已重构
class MusicSequenceVerificationFailed(MusicSequenceDecodeError):
"""音乐序列校验失败"""
def __init__(self, *args):
"""音符序列文件与其校验值不一致"""
super().__init__("音符序列文件校验失败", *args)

View File

@@ -170,7 +170,7 @@ class MusicSequence:
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 = velocity_2_distance_natural, volume_processing_function: FittingFunctionType = volume_2_distance_natural,
panning_processing_function: FittingFunctionType = panning_2_rotation_linear, 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] = {},
@@ -273,7 +273,7 @@ class MusicSequence:
8 : (stt_index := 8 + (group_1 >> 10)) 8 : (stt_index := 8 + (group_1 >> 10))
].decode("GB18030") ].decode("GB18030")
channels_: MineNoteChannelType = empty_midi_channels(default_staff=[]) channels_: MineNoteChannelType = enumerated_stuff_copy(staff=[])
total_note_count = 0 total_note_count = 0
if verify: if verify:
_header_index = stt_index _header_index = stt_index
@@ -415,7 +415,7 @@ class MusicSequence:
_t6_buffer = _t2_buffer = 0 _t6_buffer = _t2_buffer = 0
_channel_inst_chart: Dict[str, Dict[str, int]] = {} _channel_inst_chart: Dict[str, Dict[str, int]] = {}
channels_: MineNoteChannelType = empty_midi_channels(default_staff=[]) channels_: MineNoteChannelType = enumerated_stuff_copy(staff=[])
for i in range(total_note_count): for i in range(total_note_count):
if verify: if verify:
@@ -525,7 +525,7 @@ class MusicSequence:
music_name_ = bytes_buffer_in[ music_name_ = bytes_buffer_in[
8 : (stt_index := 8 + (group_1 >> 10)) 8 : (stt_index := 8 + (group_1 >> 10))
].decode("GB18030") ].decode("GB18030")
channels_: MineNoteChannelType = empty_midi_channels(default_staff=[]) channels_: MineNoteChannelType = enumerated_stuff_copy(staff=[])
for channel_index in channels_.keys(): for channel_index in channels_.keys():
for i in range( for i in range(
int.from_bytes( int.from_bytes(
@@ -568,7 +568,7 @@ class MusicSequence:
music_name_ = bytes_buffer_in[ music_name_ = bytes_buffer_in[
8 : (stt_index := 8 + (group_1 >> 10)) 8 : (stt_index := 8 + (group_1 >> 10))
].decode("utf-8") ].decode("utf-8")
channels_: MineNoteChannelType = empty_midi_channels(default_staff=[]) channels_: MineNoteChannelType = enumerated_stuff_copy(staff=[])
for channel_index in channels_.keys(): for channel_index in channels_.keys():
for i in range( for i in range(
int.from_bytes( int.from_bytes(
@@ -820,7 +820,7 @@ class MusicSequence:
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 = velocity_2_distance_natural, vol_processing_function: FittingFunctionType = volume_2_distance_natural,
pan_processing_function: FittingFunctionType = panning_2_rotation_trigonometric, 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]]:
@@ -860,10 +860,10 @@ class MusicSequence:
raise ZeroSpeedError("播放速度不得为零,应为 (0,1] 范围内的实数。") raise ZeroSpeedError("播放速度不得为零,应为 (0,1] 范围内的实数。")
# 一个midi中仅有16个通道 我们通过通道来识别而不是音轨 # 一个midi中仅有16个通道 我们通过通道来识别而不是音轨
midi_channels: MineNoteChannelType = empty_midi_channels(default_staff=[]) midi_channels: MineNoteChannelType = enumerated_stuff_copy(staff=[])
channel_controler: Dict[int, Dict[str, int]] = empty_midi_channels( channel_controler: Dict[int, Dict[str, int]] = enumerated_stuff_copy(
default_staff={ staff={
MIDI_PROGRAM: default_program_value, MIDI_PROGRAM: default_program_value,
MIDI_VOLUME: default_volume_value, MIDI_VOLUME: default_volume_value,
MIDI_PAN: 64, MIDI_PAN: 64,
@@ -883,7 +883,7 @@ class MusicSequence:
int, int,
] ]
], ],
] = empty_midi_channels(default_staff=[]) ] = enumerated_stuff_copy(staff=[])
note_queue_B: Dict[ note_queue_B: Dict[
int, int,
List[ List[
@@ -892,7 +892,7 @@ class MusicSequence:
int, int,
] ]
], ],
] = empty_midi_channels(default_staff=[]) ] = enumerated_stuff_copy(staff=[])
lyric_cache: List[Tuple[int, str]] = [] lyric_cache: List[Tuple[int, str]] = []
@@ -1099,7 +1099,7 @@ class MidiConvert(MusicSequence):
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 = velocity_2_distance_natural, vol_processing_function: FittingFunctionType = volume_2_distance_natural,
pan_processing_function: FittingFunctionType = panning_2_rotation_trigonometric, 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] = {},
@@ -1182,7 +1182,7 @@ class MidiConvert(MusicSequence):
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 = velocity_2_distance_natural, vol_processing_func: FittingFunctionType = volume_2_distance_natural,
pan_processing_func: FittingFunctionType = panning_2_rotation_linear, 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] = {},

View File

@@ -39,31 +39,15 @@ from Musicreater.constants import (
MM_INSTRUMENT_DEVIATION_TABLE, MM_INSTRUMENT_DEVIATION_TABLE,
MM_INSTRUMENT_RANGE_TABLE, MM_INSTRUMENT_RANGE_TABLE,
) )
from .old_exceptions import MusicSequenceDecodeError from Musicreater.exceptions import SingleNoteDecodeError
from Musicreater._utils import enumerated_stuffcopy_dictionary
from Musicreater.builtin_plugins.midi_read.utils import midi_inst_to_mc_sound
from .subclass import MineNote, mctick2timestr, SingleNoteBox from .subclass import MineNote, mctick2timestr, SingleNoteBox
from .old_types import MidiInstrumentTableType, MineNoteChannelType, FittingFunctionType from .old_types import MidiInstrumentTableType, MineNoteChannelType, FittingFunctionType
def empty_midi_channels(
channel_count: int = 17, default_staff: Any = {}
) -> Dict[int, Any]:
"""
空MIDI通道字典
"""
return dict(
(
i,
(
default_staff.copy()
if isinstance(default_staff, (dict, list))
else default_staff
),
) # 这告诉我们你不能忽略任何一个复制的序列因为它真的我哭死折磨我一整天全在这个bug上了
for i in range(channel_count)
)
def inst_to_sould_with_deviation( def inst_to_sould_with_deviation(
instrumentID: int, instrumentID: int,
reference_table: MidiInstrumentTableType, reference_table: MidiInstrumentTableType,
@@ -99,34 +83,6 @@ def inst_to_sould_with_deviation(
) )
def midi_inst_to_mc_sound(
instrumentID: int,
reference_table: MidiInstrumentTableType,
default_instrument: str = "note.flute",
) -> str:
"""
返回midi的乐器ID对应的我的世界乐器名
Parameters
----------
instrumentID: int
midi的乐器ID
reference_table: Dict[int, Tuple[str, int]]
转换乐器参照表
default_instrument: str
查无此乐器时的替换乐器
Returns
-------
str我的世界乐器名
"""
return reference_table.get(
instrumentID,
default_instrument,
)
def minenote_to_command_parameters( def minenote_to_command_parameters(
mine_note: MineNote, mine_note: MineNote,
pitch_deviation: float = 0, pitch_deviation: float = 0,
@@ -175,85 +131,6 @@ def minenote_to_command_parameters(
) )
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_: FittingFunctionType,
panning_processing_method_: FittingFunctionType,
note_table_replacement: Dict[str, str] = {},
lyric_line: str = "",
) -> MineNote:
"""
将Midi信息转为我的世界音符对象
Parameters
------------
inst_: int
乐器编号
note_: int
音高编号音符编号
percussive_: bool
是否作为打击乐器启用
volume_: int
音量
velocity_: int
力度
panning_: int
声相偏移
start_time_: int
音符起始时间微秒
duration_: int
音符持续时间微秒
play_speed: float
曲目播放速度
midi_reference_table: Dict[int, str]
转换对照表
volume_processing_method_: Callable[[float], float]
音量处理函数
panning_processing_method_: Callable[[float], float]
立体声相偏移处理函数
note_table_replacement: Dict[str, str]
音符替换表定义 Minecraft 音符字串的替换
lyric_line: str
该音符的歌词
Returns
---------
MineNote
我的世界音符对象
"""
mc_sound_ID = midi_inst_to_mc_sound(
inst_,
midi_reference_table,
"note.bd" if percussive_ else "note.flute",
)
return MineNote(
mc_sound_name=note_table_replacement.get(mc_sound_ID, mc_sound_ID),
midi_pitch=note_,
midi_velocity=velocity_,
start_time=(tk := int(start_time_ / float(play_speed) / 50000)),
last_time=round(duration_ / float(play_speed) / 50000),
mass_precision_time=round((start_time_ / float(play_speed) - tk * 50000) / 800),
is_percussion=percussive_,
distance=volume_processing_method_(volume_),
azimuth=(panning_processing_method_(panning_), 0),
extra_information={
"LYRIC_TEXT": lyric_line,
"VOLUME_VALUE": volume_,
"PIN_VALUE": panning_,
},
)
def midi_msgs_to_minenote_using_kami_respack( def midi_msgs_to_minenote_using_kami_respack(
inst_: int, # 乐器编号 inst_: int, # 乐器编号
note_: int, note_: int,
@@ -549,11 +426,10 @@ def load_decode_fsq_flush_release(
) )
except Exception as _err: except Exception as _err:
# print(bytes_buffer_in[stt_index:end_index]) # print(bytes_buffer_in[stt_index:end_index])
raise MusicSequenceDecodeError( raise SingleNoteDecodeError(
_err,
"所截取的音符码之首个字节:", "所截取的音符码之首个字节:",
_first_byte, _first_byte,
) ) from _err
def load_decode_msq_flush_release( def load_decode_msq_flush_release(
@@ -600,8 +476,8 @@ def load_decode_msq_flush_release(
_total_note_count = 1 _total_note_count = 1
_channel_infos = empty_midi_channels( _channel_infos = enumerated_stuffcopy_dictionary(
default_staff={"NOW_INDEX": 0, "NOTE_COUNT": 0, "HAVE_READ": 0, "END_INDEX": -1} staff={"NOW_INDEX": 0, "NOTE_COUNT": 0, "HAVE_READ": 0, "END_INDEX": -1}
) )
for __channel_index in _channel_infos.keys(): for __channel_index in _channel_infos.keys():
@@ -730,7 +606,7 @@ def load_decode_msq_flush_release(
_total_note_count -= 1 _total_note_count -= 1
except Exception as _err: except Exception as _err:
# print(channels_) # print(channels_)
raise MusicSequenceDecodeError("难以定位的解码错误", _err) raise SingleNoteDecodeError("难以定位的解码错误") from _err
if not _read_in_note_list: if not _read_in_note_list:
break break
# _note_list.append # _note_list.append

View File

@@ -21,7 +21,7 @@ print(t := msct.midi_2_music_plugin.load(Path("./resources/测试片段.mid"), N
t = _global_plugin_registry._music_input_plugins["midi_2_music_plugin"].load( t = _global_plugin_registry._music_input_plugins["midi_2_music_plugin"].load(
Path("./resources/测试片段.mid"), Path("./resources/测试片段.mid"),
MidiImportConfig( MidiImportConfig(
speed=1.0, speed_multiplier=1.0,
), ),
) )
# 或者 # 或者