小重构:音量vs距离新的计算方案

This commit is contained in:
2026-07-22 08:23:16 +08:00
parent df3519ebc5
commit debd42c633
15 changed files with 673 additions and 217 deletions
+4
View File
@@ -33,6 +33,10 @@ vanilla_assets/
/*.mid
/*.midi
# Fack Apple
.DS_Store
# Fack Microsoft
Thumbs.db
# Byte-compiled / optimized
__pycache__/
+6 -1
View File
@@ -27,7 +27,7 @@ https://gitee.com/TriM-Organization/Musicreater/blob/master/LICENSE.md。
# 若需转载或借鉴 许可声明请查看仓库目录下的 License.md
__version__ = "3.0.0-alpha"
__version__ = "3.0.0-beta"
__author__ = (
("金羿", "Eilles"),
@@ -44,6 +44,8 @@ from .data import (
SoundAtmos,
MineNote,
CurvableParam,
Instrument,
DefaultInstrument,
)
from .plugins import load_plugin_from_module
@@ -64,7 +66,10 @@ __all__ = [
"SoundAtmos",
"MineNote",
"CurvableParam",
"Instrument",
# 工程项目相关
"load_plugin_from_module",
"MusiCreater",
# 默认乐器
"DefaultInstrument",
]
+44
View File
@@ -62,3 +62,47 @@ def incremental_save_path(
).exists():
now_appendix += 1
return ss_path
def volume_weight_to_sound_distance(
max_weight: float, minimal_loadness: int, weight: float, loadness: int
) -> float:
"""
通过音量权计算声源距离
Parameters
==========
max_weight: float
全曲最大音量权
minimal_loadness: int
曲目中响度最小乐器的响度
weight: float
当前乐器的音量权
loadness: int
当前乐器的响度
Returns
=======
float
声源距离
Notes
=====
以下关于距离的计算逻辑请见[计算过程](./resources/响度公式/计算过程.jpg)\n
关于响度的计算方法请见[此脚本](./resources/experiments/exam_loadness.py)\n
最终公式为:\n
声源距离 = 48 - (48 * 音量权 * ( 10 ** ((当前乐器的响度 - 响度最小乐器的响度) / 20 ))) / 全曲最大音量权\n
D_t = 48 - \\frac{48 \\cdot P_t \\cdot 10^{\\frac{V_t - V_x}{20}}}{P_m}
由于我们存储的响度都是 百-LUFS,所以下面的计算除数为 2000
"""
if weight == 0:
return 48
elif weight == max_weight:
# print("音量权最大,差比:", ((loadness - minimal_loadness) / 2000), ",率:", (1 - pow(10, (minimal_loadness - loadness)/2000)))
return 48 * (1 - pow(10, (minimal_loadness - loadness) / 2000))
else:
return 48 * (
1 - pow(10, (minimal_loadness - loadness) / 2000) * weight / max_weight
)
@@ -33,20 +33,13 @@ from .constants import (
MM_NBS_PERCUSSION_INSTRUMENT_TABLE,
)
from .utils import (
volume_2_distance_natural,
volume_2_distance_straight,
panning_2_rotation_linear,
panning_2_rotation_trigonometric,
)
from .utils import panning_2_rotation_linear, panning_2_rotation_trigonometric
__all__ = [
# 插件参数和插件本体类
"MidiImportConfig",
"MidiImport2MusicPlugin",
# 内置的拟合函数
"volume_2_distance_straight",
"volume_2_distance_natural",
"panning_2_rotation_linear",
"panning_2_rotation_trigonometric",
# Midi 相关默认值
+69 -45
View File
@@ -23,7 +23,15 @@ from enum import Enum
from pathlib import Path
from typing import BinaryIO, Optional, Dict, List, Callable, Tuple, Mapping
from Musicreater import SingleMusic, SingleTrack, SingleNote, SoundAtmos
from Musicreater import (
SingleMusic,
SingleTrack,
SingleNote,
SoundAtmos,
CurvableParam,
ParamCurve,
Instrument,
)
from Musicreater.plugins import (
music_input_plugin,
PluginConfig,
@@ -31,7 +39,9 @@ from Musicreater.plugins import (
PluginTypes,
MusicInputPluginBase,
)
from Musicreater.constants import SoundInformation, MC_INSTRUMENT_SOUND_INFO_TABLE
from Musicreater.exceptions import ZeroSpeedError, IllegalMinimumVolumeError
from Musicreater.paramcurve import Keyframe as ParamKeyframe
from Musicreater._utils import enumerated_stuffcopy_dictionary
from .constants import (
@@ -46,7 +56,6 @@ from .exceptions import (
LyricMismatchError,
)
from .utils import (
volume_2_distance_natural,
panning_2_rotation_trigonometric,
midi_msgs_to_noteinfo,
)
@@ -76,9 +85,10 @@ class MidiImportConfig(PluginConfig):
pitched_note_reference_table: Mapping[int, str] = None # type: ignore
percussion_note_reference_table: Mapping[int, str] = None # type: ignore
note_replacement_table: Mapping[str, str] = None # type: ignore
instrument_information_table: Mapping[str, SoundInformation] = None # type: ignore
# 参数转换函数
volume_process_function: Callable[[float], float] = volume_2_distance_natural
# 参数转换相关
keep_velocity_to_param_curve: bool = True
panning_processing_function: Callable[[float], float] = (
panning_2_rotation_trigonometric
)
@@ -104,6 +114,11 @@ class MidiImportConfig(PluginConfig):
self.note_replacement_table = (
self.note_replacement_table if self.note_replacement_table else {}
)
self.instrument_information_table = (
self.instrument_information_table
if self.instrument_information_table
else MC_INSTRUMENT_SOUND_INFO_TABLE
)
class ControlerKeys(Enum):
@@ -140,6 +155,8 @@ class TrackDivisionDict(
division_by_volume: bool = False
division_by_panning: bool = False
create_volume_curve: bool = True
def __init__(
self,
*args,
@@ -173,7 +190,9 @@ class TrackDivisionDict(
try:
return super().__getitem__(key)
except KeyError:
self[key] = SingleTrack()
self[key] = SingleTrack(precise_time=True)
if self.create_volume_curve:
self[key].argument_curves[CurvableParam.VOLUME] = ParamCurve()
return self[key]
@@ -401,50 +420,51 @@ class MidiImport2MusicPlugin(MusicInputPluginBase):
# 更新结果信息
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=_volume,
velocity=_velocity,
panning=_panning,
start_time=_start_ms, # 微秒
duration=microseconds - _start_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.encode("latin1").decode(
config.string_encoding
)
if config.string_encoding
else _lyric
),
)
that_note, sound_name, 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=_volume,
velocity=_velocity,
panning=_panning,
start_time=_start_ms, # 微秒
duration=microseconds - _start_ms, # 微秒
play_speed=config.speed_multiplier,
midi_reference_table=(
config.percussion_note_reference_table
if _is_percussion
else config.pitched_note_reference_table
),
panning_processing_method=config.panning_processing_function,
note_table_replacement=config.note_replacement_table,
lyric_line=(
_lyric.encode("latin1").decode(config.string_encoding)
if config.string_encoding
else _lyric
),
)
# print(that_note.start_time, end=", ")
divided_tracks[
now_track = divided_tracks[
(
track_no,
msg.channel,
sound_name,
orign_distance,
_volume,
sound_rotation,
)
].add(that_note)
]
now_track.add(that_note)
if config.keep_velocity_to_param_curve:
now_track.argument_curves[CurvableParam.VOLUME].add_key( # type: ignore
time=that_note.precise_start_time,
value=_velocity / 127,
)
# 更新统计信息
note_count += 1
@@ -500,11 +520,15 @@ class MidiImport2MusicPlugin(MusicInputPluginBase):
): # 音轨编号
every_single_track.name = track_name
if track_properties[2]: # 乐器名称
every_single_track.instrument = track_properties[2]
if track_properties[3]: # 音量
every_single_track.sound_position.sound_distance = track_properties[3]
every_single_track.instrument = Instrument(
track_properties[2],
config.instrument_information_table[track_properties[2]]["C-LUFS"],
)
if track_properties[3]: # 音量权
every_single_track.__volume_weight = track_properties[3]
if track_properties[4]: # 声相
every_single_track.sound_position.sound_azimuth = track_properties[4]
final_music.append(every_single_track)
every_single_track.sound_position = track_properties[4]
super(SingleMusic, final_music).append(every_single_track)
final_music._calc_values()
# print("所有声源距离表:",{i:t.sound_distance for i, t in enumerate(final_music)})
return final_music
+2 -51
View File
@@ -24,50 +24,6 @@ from typing import Callable, Dict, List, Optional, Sequence, Tuple, Mapping
from Musicreater import SingleNote, SoundAtmos
def volume_2_distance_natural(
vol: float,
) -> float:
"""
Midi 力度值/音量值拟合成的距离函数,一种更加自然的听感?
Parameters
----------
vol: int
Midi 音符力度值(0~127
Returns
-------
float播放中心到玩家的距离
"""
return (
-8.081720684086314
* math.log(
vol + 14.579508825070013,
)
+ 37.65806375944386
if vol < 60.64
else 0.2721359356095803 * ((vol + 2592.272889454798) ** 1.358571233418649)
+ -6.313841334963396 * (vol + 2592.272889454798)
+ 4558.496367823575
)
def volume_2_distance_straight(vol: float) -> float:
"""
Midi 力度值/音量值拟合成的距离函数,线性转换
Parameters
----------
vol: int
Midi 音符力度值(0~127
Returns
-------
float播放中心到玩家的距离
"""
return (vol + 1) / -8 + 16
def panning_2_rotation_linear(pan_: float) -> float:
"""
Midi 左右平衡偏移值线性转为声源旋转角度
@@ -147,11 +103,10 @@ def midi_msgs_to_noteinfo(
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]]:
) -> Tuple[SingleNote, str, Tuple[float, float]]:
"""
将 Midi信息转为音符对象
@@ -177,8 +132,6 @@ def midi_msgs_to_noteinfo(
曲目播放速度
midi_reference_table: Dict[int, str]
转换对照表
volume_processing_method: Callable[[float], float]
音量处理函数
panning_processing_method: Callable[[float], float]
立体声相偏移处理函数
note_table_replacement: Dict[str, str]
@@ -191,7 +144,6 @@ def midi_msgs_to_noteinfo(
Tuple[
MineNote我的世界音符对象,
str我的世界声音名,
float播放中心到玩家的距离,
Tuple[float, float]声源旋转角度
]
"""
@@ -204,7 +156,6 @@ def midi_msgs_to_noteinfo(
return (
SingleNote(
note_pitch=note,
note_volume=velocity, # 需要重新设计
start_tick=(tk := int(start_time / float(play_speed) / 50000)),
keep_tick=round(duration / float(play_speed) / 50000),
mass_precision_time=round(
@@ -212,11 +163,11 @@ def midi_msgs_to_noteinfo(
),
extra_information={
"LYRIC_TEXT": lyric_line,
"VELOCITY_VALUE": velocity,
"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),
)
@@ -510,7 +510,6 @@ class NoteDataConvert2CommandPlugin(LibraryPluginBase):
(
relative_coordinates,
volume_percentage,
mc_pitch,
) = minenote_to_command_parameters(
note,
@@ -527,10 +526,9 @@ class NoteDataConvert2CommandPlugin(LibraryPluginBase):
.replace("(", r"{")
.replace(")", r"}")
)
+ r"playsound {} @s ^{} ^{} ^{} {} {} {}".format(
+ "playsound {} @s ^{} ^{} ^{} 3.0 {} {}".format(
track.instrument,
*relative_coordinates,
volume_percentage,
1.0 if note.percussive else mc_pitch,
minimum_volume,
)
@@ -595,7 +593,6 @@ class NoteDataConvert2CommandPlugin(LibraryPluginBase):
(
relative_coordinates,
volume_percentage,
mc_pitch,
) = minenote_to_command_parameters(
note,
@@ -606,10 +603,9 @@ class NoteDataConvert2CommandPlugin(LibraryPluginBase):
MineCommand(
command=(
execute_command_head.format(player_selector)
+ "playsound {} @s ^{} ^{} ^{} {} {} {}".format(
+ "playsound {} @s ^{} ^{} ^{} 3.0 {} {}".format(
note.instrument,
*relative_coordinates,
volume_percentage,
1.0 if note.percussive else mc_pitch,
minimum_volume,
)
@@ -38,7 +38,6 @@ def minenote_to_command_parameters(
pitch_deviation: float = 0,
) -> Tuple[
Tuple[float, float, float],
float,
Union[float, Literal[None]],
]:
"""
@@ -53,13 +52,12 @@ def minenote_to_command_parameters(
返回
----
tuple[float, float, float], float, float
播放视角坐标, 指令音量参数, 指令音调参数
tuple[float, float, float], float
播放视角坐标, 指令音调参数
"""
return (
mine_note.position.position_displacement,
mine_note.volume / 127,
(
None
if mine_note.percussive
+14 -7
View File
@@ -17,7 +17,7 @@ Terms & Conditions: License.md in the root directory
# 若需转载或借鉴 许可声明请查看仓库目录下的 License.md
# from .types import Dict, List, Tuple, MidiInstrumentTableType, MidiNoteNameTableType
from typing import Dict, List, Tuple
from typing import Dict, List, Tuple, TypedDict
x = "x"
"""
@@ -438,7 +438,17 @@ MC_INSTRUMENT_BLOCKS_TABLE: Dict[str, Tuple[str, ...]] = {
}
"""MC乐器对音符盒下垫方块对照表"""
MC_INSTRUMENT_SOUND_INFO_TABLE: Dict[str, Dict[str, float]] = {
SoundInformation = TypedDict(
"SoundInformation",
{
"C-LUFS": int,
"MS": float,
"SR": int,
},
)
"""MC乐器信息类定义"""
MC_INSTRUMENT_SOUND_INFO_TABLE: Dict[str, SoundInformation] = {
"note.bass": {"C-LUFS": -1178, "MS": 532.0, "SR": 7218},
"note.bassattack": {"C-LUFS": -1188, "MS": 484.8, "SR": 7921},
"note.snare": {"C-LUFS": -1173, "MS": 80.0, "SR": 48000},
@@ -514,6 +524,7 @@ MC_INSTRUMENT_VOLUME_BALANCE_TABLE: Dict[str, float] = {
"mob.zombie.wood": 0.32247793193163765,
}
"""乐器响度平衡表,倍率"""
# 上表用处不大
MC_EILLES_RT261_INSTRUMENT_REPLACE_TABLE: Dict[str, str] = {
"note.trumpet": "note.flute",
@@ -625,8 +636,4 @@ MM_INSTRUMENT_DEVIATION_TABLE: Dict[str, int] = {
# 金羿ELS 音符方块对照表
MN_EILLES_NOTE_BLOCK_TABLE: Dict[int, str] = {
}
MN_EILLES_NOTE_BLOCK_TABLE: Dict[int, str] = {}
+472 -94
View File
@@ -29,6 +29,7 @@ from typing import (
Optional,
Any,
List,
SupportsIndex,
Tuple,
Union,
Dict,
@@ -42,10 +43,17 @@ from typing import (
Hashable,
TypeVar,
Mapping,
overload,
)
from enum import Enum
from .exceptions import SingleNoteDecodeError, ParameterTypeError, ParameterValueError
from ._utils import volume_weight_to_sound_distance
from .exceptions import (
SingleNoteDecodeError,
ParameterTypeError,
ParameterValueError,
ParameterCacheUnfreshError,
)
from .paramcurve import ParamCurve
T = TypeVar("T")
@@ -149,6 +157,44 @@ class SoundAtmos:
return SoundAtmos(self.sound_distance, self.sound_azimuth)
@dataclass
class Instrument:
"""乐器类"""
name: str
"""乐器名称"""
loadness: int
"""乐器响度 单位`百 LUFS`"""
def copy(self) -> "Instrument":
return Instrument(self.name, self.loadness)
class DefaultInstrument(Enum):
BASS = Instrument("note.bass", -1178)
BASSATTACK = Instrument("note.bassattack", -1188)
SNARE = Instrument("note.snare", -1173)
HAT = Instrument("note.hat", -1480)
BD = Instrument("note.bd", -957)
BELL = Instrument("note.bell", -2510)
FLUTE = Instrument("note.flute", -2076)
CHIME = Instrument("note.chime", -2984)
GUITAR = Instrument("note.guitar", -2539)
XYLOPHONE = Instrument("note.xylophone", -2419)
IRON_XYLOPHONE = Instrument("note.iron_xylophone", -2078)
COW_BELL = Instrument("note.cow_bell", -1427)
DIDGERIDOO = Instrument("note.didgeridoo", -2062)
BIT = Instrument("note.bit", -2677)
BANJO = Instrument("note.banjo", -2207)
PLING = Instrument("note.pling", -1528)
TRUMPET = Instrument("note.trumpet", -1678)
TRUMPET_EXPOSED = Instrument("note.trumpet_exposed", -1682)
TRUMPET_OXIDIZED = Instrument("note.trumpet_oxidized", -2225)
TRUMPET_WEATHERED = Instrument("note.trumpet_weathered", -1875)
HARP = Instrument("note.harp", -1448)
@dataclass(init=False)
class SingleNote:
"""存储单个音符的类"""
@@ -156,9 +202,6 @@ class SingleNote:
midi_pitch: int
"""Midi 音高"""
volume: int
"""力度/播放响度 0~127 百廿七分比"""
start_time: int
"""开始之时 命令刻"""
@@ -174,7 +217,6 @@ class SingleNote:
def __init__(
self,
note_pitch: Optional[int],
note_volume: int,
start_tick: int,
keep_tick: int,
mass_precision_time: int = 0,
@@ -187,8 +229,6 @@ class SingleNote:
------------
midi_pitch: int
Midi 音高
note_volume: int
响度或曰力度(百廿七分比, 0~127)
start_time: int
开始之时(命令刻)
注:此处的时间是用从乐曲开始到当前的刻数
@@ -208,8 +248,6 @@ class SingleNote:
self.midi_pitch: int = 66 if note_pitch is None else note_pitch
"""Midi 音高"""
self.volume: int = note_volume
"""响度(力度)"""
self.start_time: int = start_tick
"""开始之时 命令刻"""
self.duration: int = keep_tick
@@ -228,19 +266,17 @@ class SingleNote:
def decode(cls, code_buffer: bytes, is_high_time_precision: bool = True):
"""自字节码析出 SingleNote 类"""
duration_ = (
group_1 := int.from_bytes(code_buffer[:6], "big")
group_1 := int.from_bytes(code_buffer[:5], "big")
) & 0b11111111111111111
start_tick_ = (group_1 := group_1 >> 17) & 0b11111111111111111
note_volume_ = (group_1 := group_1 >> 17) & 0b1111111
note_pitch_ = (group_1 := group_1 >> 7) & 0b1111111
start_tick_ = (group_1 := group_1 >> 17) & 0b111111111111111111
note_pitch_ = (group_1 := group_1 >> 18) & 0b1111111
try:
return cls(
note_pitch=note_pitch_,
note_volume=note_volume_,
start_tick=start_tick_,
keep_tick=duration_,
mass_precision_time=code_buffer[6] if is_high_time_precision else 0,
mass_precision_time=code_buffer[5] if is_high_time_precision else 0,
)
except Exception as e:
# 我也不知道为什么这里要放一个异常处理
@@ -275,20 +311,15 @@ class SingleNote:
# SingleNote 的字节码
# note_pitch 7 位 支持到 127
# volume 长度 7 位 支持到 127
# start_tick 17 位 支持到 131071 即 109.22583 分钟 合 1.8204305 小时
# start_tick 18 位 支持到 131071 即 109.22583 分钟 合 1.8204305 小时
# duration 17 位 支持到 131071 即 109.22583 分钟 合 1.8204305 小时
# 共 48 位 合 6 字节
# 共 40 位 合 5 字节
# high_time_precision(可选)长度 8 位 支持到 255 合 1 字节 单位为 1/5000 秒]
return (
(
(
((((self.midi_pitch << 7) + self.volume) << 17) + self.start_time)
<< 17
)
+ self.duration
((((self.midi_pitch) << 18) + self.start_time) << 17) + self.duration
).to_bytes(6, "big")
# + self.track_no.to_bytes(1, "big")
+ (
@@ -320,37 +351,38 @@ class SingleNote:
return self.extra_info.get(key, default)
def stringize(self, include_extra_data: bool = False) -> str:
return "TrackedNote(Pitch = {}, Volume = {}, StartTick = {}, Duration = {}, TimeOffset = {}".format(
return "Note(Pitch = {}, StartTick = {}, Duration = {}, TimeOffset = {}".format(
self.midi_pitch,
self.volume,
self.start_time,
self.duration,
self.high_precision_start_time,
) + (
", ExtraData = {})".format(self.extra_info) if include_extra_data else ")"
)
) + (", ExtraData = {})".format(self.extra_info) if include_extra_data else ")")
def copy(self) -> "SingleNote":
def copy(
self,
note_pitch: Optional[int] = None,
start_tick: Optional[int] = None,
keep_tick: Optional[int] = None,
mass_precision_time: Optional[int] = None,
) -> "SingleNote":
return SingleNote(
note_pitch=self.midi_pitch,
note_volume=self.volume,
start_tick=self.start_time,
keep_tick=self.duration,
mass_precision_time=self.high_precision_start_time,
note_pitch=note_pitch or self.midi_pitch,
start_tick=start_tick or self.start_time,
keep_tick=keep_tick or self.duration,
mass_precision_time=mass_precision_time or self.high_precision_start_time,
)
# def __list__(self) -> List[int]:
# 我不认为这个类应当被作为列表使用
def __bool__(self) -> bool:
return bool(self.volume)
return self.duration > 0
def __tuple__(
self,
) -> Tuple[int, int, int, int, int]:
) -> Tuple[int, int, int, int]:
return (
self.midi_pitch,
self.volume,
self.start_time,
self.duration,
self.high_precision_start_time,
@@ -359,7 +391,6 @@ class SingleNote:
def __dict__(self):
return {
"Pitch": self.midi_pitch,
"Volume": self.volume,
"StartTick": self.start_time,
"Duration": self.duration,
"TimeOffset": self.high_precision_start_time,
@@ -395,8 +426,6 @@ class MineNote:
"""Midi 音高"""
instrument: str
"""乐器标识"""
volume: float
"""力度或曰播放音量、响度 0~127 百廿七分比"""
start_tick: int
"""开始之时 命令刻"""
duration_tick: int
@@ -418,30 +447,57 @@ class MineNote:
cls,
note: SingleNote,
note_instrument: str,
note_volume_weight: float,
is_persiced_time: bool,
is_percussive_note: bool,
sound_position: SoundAtmos,
adjust_note_pitch: float = 0.0,
adjust_note_volume: float = 0.0,
adjust_note_volume_weight: float = 0.0,
adjust_note_sound_distance: float = 0.0,
adjust_note_leftright_panning_degree: float = 0.0,
adjust_note_updown_panning_degree: float = 0.0,
) -> "MineNote":
"""从SingleNote对象创建MineNote对象"""
sound_position.sound_distance += adjust_note_sound_distance
sound_position.sound_azimuth = (
sound_position.sound_azimuth[0] + adjust_note_leftright_panning_degree,
sound_position.sound_azimuth[1] + adjust_note_updown_panning_degree,
)
return cls(
pitch=note.midi_pitch + adjust_note_pitch,
instrument=note_instrument,
volume=note.volume + adjust_note_volume,
start_tick=note.start_time,
duration_tick=note.duration,
start_time_offset=note.high_precision_start_time if is_persiced_time else 0,
percussive=is_percussive_note,
position=sound_position,
position=(
SoundAtmos(
sound_position.sound_distance
+ adjust_note_sound_distance
+ (
(
(
(
(sound_position.sound_distance - 48)
* (note_volume_weight + adjust_note_volume_weight)
/ note_volume_weight
)
+ 48
)
)
if adjust_note_volume_weight
else 0
),
(
sound_position.sound_azimuth[0]
+ adjust_note_leftright_panning_degree,
sound_position.sound_azimuth[1]
+ adjust_note_updown_panning_degree,
),
)
if (
adjust_note_volume_weight
or adjust_note_sound_distance
or adjust_note_leftright_panning_degree
or adjust_note_updown_panning_degree
)
else sound_position
),
)
@@ -449,7 +505,7 @@ class CurvableParam(str, Enum):
"""可曲线化的参数 枚举类"""
PITCH = "adjust_note_pitch"
VOLUME = "adjust_note_volume"
VOLUME = "adjust_note_volume_weight"
DISTANCE = "adjust_note_sound_distance"
LR_PANNING = "adjust_note_leftright_panning_degree"
UD_PANNING = "adjust_note_updown_panning_degree"
@@ -464,7 +520,7 @@ class SingleTrack(List[SingleNote]):
is_enabled: bool = True
"""该音轨是否启用"""
instrument: str
instrument: Instrument
"""乐器ID"""
is_high_time_precision: bool
@@ -473,8 +529,14 @@ class SingleTrack(List[SingleNote]):
is_percussive: bool
"""该音轨是否标记为打击乐器轨道"""
sound_position: SoundAtmos
"""声像方位"""
__volume_weight: float = 10
"""音量权"""
__sound_distance: float = -1
"""声源距离"""
sound_position: Tuple[float, float]
"""声像方位 球坐标系角度(rV左右 rH上下)"""
argument_curves: Dict[CurvableParam, Union[ParamCurve, Literal[None]]]
"""参数曲线"""
@@ -482,20 +544,50 @@ class SingleTrack(List[SingleNote]):
extra_info: Dict[str, Any]
"""你觉得放什么好?"""
# 构建类
def __init__(
self,
*args: SingleNote,
track_name: str = "未命名音轨",
track_instrument: str = "",
track_instrument: Optional[Instrument] = None,
precise_time: bool = True,
percussion: bool = False,
sound_direction: Optional[SoundAtmos] = None,
volume_weight: float = 10.0,
sound_direction: Tuple[float, float] = (0, 0),
extra_information: Dict[str, Any] = {},
):
"""
构建音轨
Parameters
==========
*args: SingleNote
音符列表
track_name: str
音轨名称
track_instrument: str
音轨所使用的乐器名称
precise_time: bool
该音轨是否使用高精度时间
percussion: bool
该音轨的乐器是否为打击乐器(无音高调整)
volume_weight: float
音量权,用以表明音轨的音量应当占有的权重大小。值越大,听起来声音越大。
默认为 10
sound_direction: Tuple[float, float]
声源方位
注:此参数为tuple,包含两个元素,分别表示:
`rV` 发声源在竖直(上下)轴上,从玩家视角正前方开始,向顺时针旋转的角度
`rH` 发声源在水平(左右)轴上,从玩家视角正前方开始,向上
(到达玩家正上方顶点后变为向下,以此类推的旋转)旋转的角度
"""
self.name = track_name
"""音轨名称"""
self.instrument = track_instrument
self.instrument = (
track_instrument if track_instrument else DefaultInstrument.HARP.value
)
"""乐器ID"""
self.is_high_time_precision = precise_time
@@ -504,8 +596,13 @@ class SingleTrack(List[SingleNote]):
self.is_percussive = percussion
"""是否为打击乐器"""
# 如果不这样的话,所有的新的 SingleTrack 类都会有一个共同的声像方位
self.sound_position = sound_direction if sound_direction else SoundAtmos()
self.__volume_weight = volume_weight
"""音量权"""
self.__sound_distance = -1
"""声源距离"""
self.sound_position = sound_direction
"""声像方位"""
self.extra_info = extra_information if extra_information else {}
@@ -520,10 +617,11 @@ class SingleTrack(List[SingleNote]):
cls,
note_list: Iterable[SingleNote],
track_name: str = "未命名音轨",
track_instrument: str = "",
track_instrument: Optional[Instrument] = None,
precise_time: bool = True,
percussion: bool = False,
sound_direction: Optional[SoundAtmos] = None,
volume_weight: float = 10.0,
sound_direction: Tuple[float, float] = (0, 0),
extra_information: Dict[str, Any] = {},
) -> "SingleTrack":
"""从音符列表创建 SingleTrack 对象"""
@@ -533,12 +631,36 @@ class SingleTrack(List[SingleNote]):
track_instrument=track_instrument,
precise_time=precise_time,
percussion=percussion,
volume_weight=volume_weight,
sound_direction=sound_direction,
extra_information=extra_information,
)
single_track.update(note_list)
return single_track
# 类参数
@property
def volume_weight(self) -> float:
"""音量权"""
return self.__volume_weight
@property
def sound_distance(self) -> float:
"""声源距离"""
if self.__sound_distance < 0:
raise ParameterCacheUnfreshError(
"未设置声源距离(`{}`),操作时不应直接访问 `__volume_weight` 属性".format(
self.__sound_distance
)
)
return self.__sound_distance
@property
def note_amount(self) -> int:
"""音符数"""
return len(self)
def disable(self) -> None:
"""禁用音轨"""
@@ -554,6 +676,36 @@ class SingleTrack(List[SingleNote]):
self.is_enabled = not self.is_enabled
def set_volume_weight(
self, max_weight: float, minimal_loadness: int, weight
) -> float:
"""设置音量权
以下关于距离的计算逻辑请见[计算过程](./resources/响度公式/计算过程.jpg)\n
关于响度的计算方法请见[此脚本](./resources/experiments/exam_loadness.py)\n
最终公式为:\n
声源距离 = 48 - (48 * 音量权 * ( 10 ** ((当前乐器的响度 - 响度最小乐器的响度) / 20 ))) / 全曲最大音量权\n
D_t = 48 - \\frac{48 \\cdot P_t \\cdot 10^{\\frac{V_t - V_x}{20}}}{P_m}
由于我们存储的响度都是 百-LUFS,所以下面的计算除数为 2000
"""
self.__volume_weight = weight
self.__sound_distance = volume_weight_to_sound_distance(
max_weight, minimal_loadness, weight, self.instrument.loadness
)
# print(
# "收到音量权:",
# weight,
# ",最大权:",
# max_weight,
# ",设置声源距离为:",
# self.__sound_distance,
# )
return self.__sound_distance
# 类操作
def append(self, object: SingleNote) -> None:
"""
添加一个音符,推荐使用 add 方法
@@ -594,12 +746,18 @@ class SingleTrack(List[SingleNote]):
通过时间范围来复制音轨,返回一个音轨
"""
single_track = SingleTrack.from_note_list(
[x for x in self if start_time <= x.precise_start_time <= end_time],
track_instrument=self.instrument,
(
[x for x in self if start_time <= x.precise_start_time <= end_time]
if self.is_high_time_precision
else [x for x in self if start_time <= x.start_time <= end_time]
),
track_instrument=self.instrument.copy(),
precise_time=self.is_high_time_precision,
percussion=self.is_percussive,
sound_direction=self.sound_position.copy(),
volume_weight=self.volume_weight,
sound_direction=self.sound_position,
)
single_track.__sound_distance = self.sound_distance
if with_argument_curve:
single_track.argument_curves = {
item: (
@@ -616,6 +774,75 @@ class SingleTrack(List[SingleNote]):
}
return single_track
def paste(
self,
items: Iterable[SingleNote],
offset_time: int,
offset_mass_precision_time: int = 0,
is_passin_time_precise: bool = True,
):
"""
将一个音轨粘贴到当前音轨的指定时间
"""
note_iterator = items.__iter__()
first_note = next(note_iterator)
original_start_time = first_note.start_time
super().append(
first_note.copy(
start_tick=offset_time,
mass_precision_time=(
offset_mass_precision_time if self.is_high_time_precision else 0
),
)
)
if self.is_high_time_precision:
for note in note_iterator:
super().append(
note.copy(
start_tick=note.start_time
- original_start_time
+ offset_time
+ (
(
offset_mass_precision_time
+ note.high_precision_start_time
)
// 250
),
mass_precision_time=(
offset_mass_precision_time + note.high_precision_start_time
)
% 250,
)
)
else:
if is_passin_time_precise:
for note in note_iterator:
super().append(
note.copy(
start_tick=note.start_time
- original_start_time
+ offset_time
+ (
(
offset_mass_precision_time
+ note.high_precision_start_time
)
// 250
)
)
)
else:
for note in note_iterator:
super().append(
note.copy(
start_tick=note.start_time
- original_start_time
+ offset_time
)
)
self.sort() # =========================== TODO 需要优化
def delete(
self, start_time: float, end_time: float, with_argument_curve: bool = False
):
@@ -636,6 +863,35 @@ class SingleTrack(List[SingleNote]):
):
del self[j - i]
# 音符操作
@property
def notes(self) -> List[SingleNote]:
"""音符列表"""
return self
@property
def minenotes(self) -> Iterator[MineNote]:
"""
直接返回当前音轨所有音符的我的世界数据形式
"""
return (
MineNote.from_single_note(
note=_note,
note_instrument=self.instrument.name,
note_volume_weight=self.volume_weight,
is_persiced_time=self.is_high_time_precision,
is_percussive_note=self.is_percussive,
sound_position=SoundAtmos(self.sound_distance, self.sound_position),
**{
item.value: argcrv.value_at(_note.precise_start_time)
for item in CurvableParam
if (argcrv := self.argument_curves[item])
},
)
for _note in self
)
def get(self, time: int) -> Generator[SingleNote, None, None]:
"""通过确切的开始时间来获取音符"""
@@ -673,10 +929,11 @@ class SingleTrack(List[SingleNote]):
for _note in self.get_notes(range_start_time, range_end_time):
yield MineNote.from_single_note(
note=_note,
note_instrument=self.instrument,
note_instrument=self.instrument.name,
note_volume_weight=self.volume_weight,
is_persiced_time=self.is_high_time_precision,
is_percussive_note=self.is_percussive,
sound_position=self.sound_position,
sound_position=SoundAtmos(self.sound_distance, self.sound_position),
**{
item.value: argcrv.value_at(_note.precise_start_time)
for item in CurvableParam
@@ -684,36 +941,7 @@ class SingleTrack(List[SingleNote]):
},
)
@property
def note_amount(self) -> int:
"""音符数"""
return len(self)
@property
def notes(self) -> List[SingleNote]:
"""音符列表"""
return self
@property
def minenotes(self) -> Iterator[MineNote]:
"""
直接返回当前音轨所有音符的我的世界数据形式
"""
return (
MineNote.from_single_note(
note=_note,
note_instrument=self.instrument,
is_persiced_time=self.is_high_time_precision,
is_percussive_note=self.is_percussive,
sound_position=self.sound_position,
**{
item.value: argcrv.value_at(_note.precise_start_time)
for item in CurvableParam
if (argcrv := self.argument_curves[item])
},
)
for _note in self
)
# 其他
def set_info(self, key: Union[str, Sequence[str]], value: Any):
"""设置附加信息"""
@@ -759,6 +987,31 @@ class SingleMusic(List[SingleTrack]):
extra_info: Dict[str, Any]
"""这还得放东西?"""
__max_volume_weight: float = -1
"""全曲最大音量权"""
__minimal_loadness: int = 1
"""全曲响度最小的乐器采样的响度"""
# 前置方法
def _calc_values(self):
for track in self:
self.__max_volume_weight = max(
track.volume_weight, self.__max_volume_weight
)
self.__minimal_loadness = min(
track.instrument.loadness, self.__minimal_loadness
)
for track in self:
track.set_volume_weight(
max_weight=self.__max_volume_weight,
minimal_loadness=self.__minimal_loadness,
weight=track.volume_weight,
)
# 类的构建方法
def __init__(
self,
*args: SingleTrack,
@@ -788,6 +1041,8 @@ class SingleMusic(List[SingleTrack]):
super().__init__(*args)
self._calc_values()
@classmethod
def from_track_list(
cls,
@@ -810,6 +1065,95 @@ class SingleMusic(List[SingleTrack]):
single_music.extend(track_list)
return single_music
# 类操作
def append(self, track: SingleTrack) -> None:
"""
将一个音轨添加到全曲中,排列在所有音轨之后。
Append object to the end of the list.
"""
super().append(track)
self._calc_values()
def extend(self, tracks: Sequence[SingleTrack]) -> None:
"""
将一个音轨列表添加到全曲中,依顺序在所有音轨之后排列。
Extend list by appending elements from the iterable.
"""
super().extend(tracks)
self._calc_values()
def insert(self, index: int, track: SingleTrack) -> None:
"""
将一个音轨插入到全曲中,在指定位置。
Insert object before index.
"""
super().insert(index, track)
self._calc_values()
def remove(self, track: SingleTrack) -> None:
"""
从全曲中删除一个音轨。
Remove first occurrence of value.
当音轨不存在时,会抛出 `ValueError`。
Raises `ValueError` if the value is not present.
"""
super().remove(track)
self._calc_values()
def clear(self) -> None:
"""
清空全曲。
Remove all items from list.
"""
super().clear()
self.__max_volume_weight = -1
self.__minimal_loadness = 1
def pop(self, index: SupportsIndex = -1) -> SingleTrack:
"""
从全曲中删除一个音轨。
Remove and return item at index (default last).
当音轨不存在时,会抛出 `IndexError`。
Raises IndexError if list is empty or index is out of range.
"""
to_pop = super().pop(index)
self._calc_values()
return to_pop
@overload
def __setitem__(self, key: SupportsIndex, value: SingleTrack) -> None: ...
@overload
def __setitem__(self, key: slice, value: Sequence[SingleTrack]) -> None: ...
def __setitem__(self, key, value) -> None:
"""
设置一个音轨
Set self[key] to value.
"""
super().__setitem__(key, value)
self._calc_values()
def __delitem__(self, key: SupportsIndex | slice) -> None:
"""
删除一个音轨
Delete self[key].
"""
super().__delitem__(key)
self._calc_values()
def copy(self) -> "SingleMusic":
# 没想到真的会用到全曲复制……
@@ -827,6 +1171,8 @@ class SingleMusic(List[SingleTrack]):
credits=self.music_credits,
)
# 类属性
@property
def track_amount(self) -> int:
"""音轨数"""
@@ -837,6 +1183,36 @@ class SingleMusic(List[SingleTrack]):
"""音轨列表,不包含被禁用的音轨"""
return (track for track in self if track.is_enabled)
def set_loadness(self, track: Union[int, SingleTrack], weight: float):
if weight > self.__max_volume_weight:
if isinstance(track, int):
self[track].__volume_weight = weight
elif isinstance(track, SingleTrack):
if track in self:
track.__volume_weight = weight
else:
raise ParameterValueError(
"音轨:`{}`不属于此曲目".format(track.name)
)
self._calc_values()
else:
if isinstance(track, int):
self[track].set_volume_weight(
max_weight=self.__max_volume_weight,
minimal_loadness=self.__minimal_loadness,
weight=weight,
)
elif isinstance(track, SingleTrack):
track.set_volume_weight(
max_weight=self.__max_volume_weight,
minimal_loadness=self.__minimal_loadness,
weight=weight,
)
raise ParameterTypeError("音轨不得为`{}`类型".format(type(track)))
# 音符操作
@staticmethod
def yield_from_tracks(
tracks: Sequence[Iterator[T]],
@@ -938,6 +1314,8 @@ class SingleMusic(List[SingleTrack]):
sort_key=lambda x: x.start_tick,
)
# 其他
def set_info(self, key: Union[str, Sequence[str]], value: Any):
"""设置附加信息"""
if isinstance(key, str):
+9
View File
@@ -73,6 +73,15 @@ class InnerlyParameterError(MusicreaterInnerlyError):
super().__init__("传参错误 - ", *args)
class ParameterCacheUnfreshError(InnerlyParameterError):
"""参数缓存未刷新"""
def __init__(self, *args):
"""参数缓存未刷新"""
super().__init__("参数缓存未刷新:", *args)
class ParameterTypeError(InnerlyParameterError, TypeError):
"""参数类型错误"""
+1 -1
View File
@@ -268,7 +268,7 @@ class ParamCurve:
if idx < len(self._keys) and self._keys[idx].time == time:
return idx, self._keys[idx]
else:
print("[警告] ParamCurve.find_key: 找不到指定时间点所对应之关键帧")
# print("[警告] ParamCurve.find_key: 找不到指定时间点所对应之关键帧")
return idx, None
def copy(
+47
View File
@@ -15,6 +15,7 @@ Terms & Conditions: License.md in the root directory
# Email TriM-Organization@hotmail.com
# 若需转载或借鉴 许可声明请查看仓库目录下的 License.md
import math
import random
# from io import BytesIO
@@ -48,6 +49,52 @@ from .subclass import MineNote, mctick2timestr, SingleNoteBox
from .old_types import MidiInstrumentTableType, MineNoteChannelType, FittingFunctionType
def volume_2_distance_natural(
vol: float,
) -> float:
"""
Midi 力度值/音量值拟合成的距离函数,一种更加自然的听感?
Parameters
----------
vol: int
Midi 音符力度值(0~127
Returns
-------
float播放中心到玩家的距离
"""
return (
-8.081720684086314
* math.log(
vol + 14.579508825070013,
)
+ 37.65806375944386
if vol < 60.64
else 0.2721359356095803 * ((vol + 2592.272889454798) ** 1.358571233418649)
+ -6.313841334963396 * (vol + 2592.272889454798)
+ 4558.496367823575
)
def volume_2_distance_straight(vol: float) -> float:
"""
Midi 力度值/音量值拟合成的距离函数,线性转换
Parameters
----------
vol: int
Midi 音符力度值(0~127
Returns
-------
float播放中心到玩家的距离
"""
return (vol + 1) / -8 + 16
def inst_to_sould_with_deviation(
instrumentID: int,
reference_table: MidiInstrumentTableType,
Binary file not shown.

After

Width:  |  Height:  |  Size: 1003 KiB

Binary file not shown.