mirror of
https://github.com/TriM-Organization/Musicreater.git
synced 2026-07-24 01:02:32 +00:00
小重构:音量vs距离新的计算方案
This commit is contained in:
@@ -33,6 +33,10 @@ vanilla_assets/
|
||||
/*.mid
|
||||
/*.midi
|
||||
|
||||
# Fack Apple
|
||||
.DS_Store
|
||||
# Fack Microsoft
|
||||
Thumbs.db
|
||||
|
||||
# Byte-compiled / optimized
|
||||
__pycache__/
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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 相关默认值
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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):
|
||||
|
||||
@@ -73,6 +73,15 @@ class InnerlyParameterError(MusicreaterInnerlyError):
|
||||
super().__init__("传参错误 - ", *args)
|
||||
|
||||
|
||||
class ParameterCacheUnfreshError(InnerlyParameterError):
|
||||
"""参数缓存未刷新"""
|
||||
|
||||
def __init__(self, *args):
|
||||
"""参数缓存未刷新"""
|
||||
super().__init__("参数缓存未刷新:", *args)
|
||||
|
||||
|
||||
|
||||
class ParameterTypeError(InnerlyParameterError, TypeError):
|
||||
"""参数类型错误"""
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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.
Reference in New Issue
Block a user