This commit is contained in:
2026-01-22 09:23:33 +08:00
parent 1e0698c85d
commit 583ca04ac9
3 changed files with 704 additions and 40 deletions

View File

@@ -4,8 +4,6 @@
存储音·创新数据存储类
"""
# WARNING 本文件中使用之功能尚未启用
"""
版权所有 © 2025 金羿
Copyright © 2025 Eilles
@@ -18,42 +16,21 @@ Terms & Conditions: License.md in the root directory
# Email TriM-Organization@hotmail.com
# 若需转载或借鉴 许可声明请查看仓库目录下的 License.md
# WARNING 本文件中使用之功能尚未启用
from math import sin, cos, asin, radians, degrees, sqrt, atan, inf
from math import sin, cos, asin, radians, degrees, sqrt, atan, inf, ceil
from dataclasses import dataclass
from typing import Optional, Any, List, Tuple, Union, Dict, Sequence, Callable
import bisect
from typing import Optional, Any, List, Tuple, Union, Dict, Sequence, Callable, Generator
from enum import Enum
from .types import FittingFunctionType
from .constants import MC_PITCHED_INSTRUMENT_LIST
class ArgumentCurve:
base_line: float = 0
"""基线/默认值"""
default_curve: Callable[[float], float]
"""默认曲线"""
defined_curves: Dict[float, "ArgumentCurve"] = {}
"""调整后的曲线集合"""
left_border: float = 0
"""定义域左边界"""
right_border: float = inf
"""定义域右边界"""
def __init__(self, baseline: float = 0, default_function: Callable[[float], float] = lambda x: 0, function_set: Dict = {}) -> None:
pass
def __call__(self, *args: Any, **kwds: Any) -> Any:
pass
from .paramcurve import ParamCurve
class SoundAtmos:
"""声源方位类"""
sound_distance: float
"""声源距离 方块"""
@@ -66,6 +43,20 @@ class SoundAtmos:
distance: Optional[float] = None,
azimuth: Optional[Tuple[float, float]] = None,
) -> None:
"""
定义一个发声方位
Parameters
------------
distance: float
发声源距离玩家的距离(半径 `r`
注:距离越近,音量越高,默认为 0。此参数可以与音量成某种函数关系。
azimuth: tuple[float, float]
声源方位
此参数为tuple包含两个元素分别表示
`rV` 发声源在竖直(上下)轴上,从玩家视角正前方开始,向顺时针旋转的角度
`rH` 发声源在水平(左右)轴上,从玩家视角正前方开始,向上(到达玩家正上方顶点后变为向下,以此类推的旋转)旋转的角度
"""
self.sound_azimuth = (azimuth[0] % 360, azimuth[1] % 360) if azimuth else (0, 0)
"""声源方位"""
@@ -103,7 +94,7 @@ class SoundAtmos:
@property
def position_displacement(self) -> Tuple[float, float, float]:
"""声像位移"""
"""声像位移,直接可应用于我的世界的相对视角的坐标参考系中(^x ^y ^z"""
dk1 = self.sound_distance * round(cos(radians(self.sound_azimuth[1])), 8)
return (
-dk1 * round(sin(radians(self.sound_azimuth[0])), 8),
@@ -161,14 +152,6 @@ class SingleNote:
高精度的开始时间偏移量(1/1250秒)
is_percussion: bool
是否作为打击乐器
distance: float
发声源距离玩家的距离(半径 `r`
注:距离越近,音量越高,默认为 0。此参数可以与音量成某种函数关系。
azimuth: tuple[float, float]
声源方位
此参数为tuple包含两个元素分别表示
`rV` 发声源在竖直(上下)轴上,从玩家视角正前方开始,向顺时针旋转的角度
`rH` 发声源在水平(左右)轴上,从玩家视角正前方开始,向上(到达玩家正上方顶点后变为向下,以此类推的旋转)旋转的角度
extra_information: Dict[str, Any]
附加信息,尽量存储为字典
@@ -332,6 +315,17 @@ class SingleNote:
return self.start_tick > other.start_tick
class CurvableParam(str, Enum):
"""可曲线化的参数枚举类"""
PITCH = "note-pitch"
VELOCITY = "note-velocity"
VOLUME = "note-volume"
DISTANCE = "note-sound-distance"
HORIZONTAL_PANNING = "note-H-panning-degree"
VERTICAL_PANNING = "note-V-panning-degree"
class SingleTrack(list):
"""存储单个轨道的类"""
@@ -353,7 +347,7 @@ class SingleTrack(list):
sound_position: SoundAtmos
"""声像方位"""
argument_curves: Dict[str, FittingFunctionType]
argument_curves: Dict[CurvableParam, ParamCurve]
"""参数曲线"""
extra_info: Dict[str, Any]
@@ -397,6 +391,11 @@ class SingleTrack(list):
"""音符数"""
return len(self)
@property
def track_notes(self) -> List[SingleNote]:
"""音符列表"""
return self
def set_info(self, key: Union[str, Sequence[str]], value: Any):
"""设置附加信息"""
if isinstance(key, str):
@@ -416,6 +415,90 @@ class SingleTrack(list):
"""获取附加信息"""
return self.extra_info.get(key, default)
class SingleMusic(object):
def get_notes(self) -> Generator[SingleNote, Any, None]:
# TODO : 添加其他参数以及参数曲线到这里来
for note in self:
yield note
class SingleMusic(list):
"""存储单个曲子的类"""
music_name: str
"""乐曲名称"""
music_creator: str
"""本我的世界曲目的制作者"""
music_original_author: str
"""曲目的原作者"""
music_description: str
"""当前曲目的简介"""
music_credits: str
"""曲目的版权信息"""
# 感叹一下什么交冗余设计啊!(叉腰)
extra_info: Dict[str, Any]
"""这还得放东西?"""
def __init__(
self,
name: str = "未命名乐曲",
creator: str = "未命名制作者",
original_author: str = "未命名原作者",
description: str = "未命名简介",
credits: str = "未命名版权信息",
*args: SingleTrack,
extra_information: Dict[str, Any] = {},
):
self.music_name = name
"""乐曲名称"""
self.music_creator = creator
"""曲目制作者"""
self.music_original_author = original_author
"""乐曲原作者"""
self.music_description = description
"""简介"""
self.music_credits = credits
"""版权信息"""
self.extra_info = extra_information if extra_information else {}
super().__init__(*args)
@property
def track_amount(self) -> int:
"""音轨数"""
return len(self)
@property
def music_tracks(self) -> List[SingleTrack]:
"""音轨列表"""
return self
def set_info(self, key: Union[str, Sequence[str]], value: Any):
"""设置附加信息"""
if isinstance(key, str):
self.extra_info[key] = value
elif (
isinstance(key, Sequence)
and isinstance(value, Sequence)
and (k := len(key)) == len(value)
):
for i in range(k):
self.extra_info[key[i]] = value[i]
else:
# 提供简单报错就行了,如果放一堆 if 语句,降低处理速度
raise TypeError("参数类型错误;键:`{}` 值:`{}`".format(key, value))
def get_info(self, key: str, default: Any = None) -> Any:
"""获取附加信息"""
return self.extra_info.get(key, default)

580
Musicreater/paramcurve.py Normal file
View File

@@ -0,0 +1,580 @@
# -*- coding: utf-8 -*-
"""
存储音·创音轨所需的参数曲线
"""
"""
版权所有 © 2025 金羿
Copyright © 2025 Eilles
开源相关声明请见 仓库根目录下的 License.md
Terms & Conditions: License.md in the root directory
"""
# 睿乐组织 开发交流群 861684859
# Email TriM-Organization@hotmail.com
# 若需转载或借鉴 许可声明请查看仓库目录下的 License.md
# WARNING 本文件中使用之功能尚未启用
# 鉴于白谭若佬给出的建议:本功能应是处于低优先级开发的
# 因此暂时用处不大,可以稍微放一会再进行开发
# 目前用人工智能生成了部分代码,只经过简单的测试
# 可以等伶伦工作站开发出来后再进行完整的测试
from math import ceil
from dataclasses import dataclass
from typing import Optional, Any, List, Tuple
from enum import Enum
import bisect
from .types import FittingFunctionType
def _evaluate_bezier_segment(
t0: float,
v0: float,
t1: float,
v1: float,
out_tangent: Optional[Tuple[float, float]],
in_tangent: Optional[Tuple[float, float]],
u: float,
) -> float:
"""
计算贝塞尔区间 [t0, t1] 在归一化参数 u ∈ [0,1] 处的 y 值。
控制点:
P0 = (t0, v0)
P1 = (t0 + out_dt, v0 + out_dv)
P2 = (t1 - in_dt, v1 - in_dv) ← 注意in_tangent 是相对于 t1 的偏移
P3 = (t1, v1)
"""
# 默认控制点:退化为线性
p0 = (t0, v0)
p3 = (t1, v1)
if out_tangent is not None:
p1 = (t0 + out_tangent[0], v0 + out_tangent[1])
else:
p1 = p0 # 无出手柄 → 与起点重合
if in_tangent is not None:
p2 = (t1 - in_tangent[0], v1 - in_tangent[1])
else:
p2 = p3 # 无入手柄 → 与终点重合
# 三次贝塞尔 y(t)
mt = 1.0 - u
return mt**3 * p0[1] + 3 * mt**2 * u * p1[1] + 3 * mt * u**2 * p2[1] + u**3 * p3[1]
class InterpolationMethod:
"""
预定义的标准化插值函数集合。所有函数接受归一化输入 u ∈ [0,1],返回 v ∈ [0,1]。
"""
@staticmethod
def linear(u: float) -> float:
"""
线性插值。
Parameters
----------
u : float
归一化时间,范围 [0, 1]。
Returns
-------
float
插值权重,范围 [0, 1]。
"""
return u
@staticmethod
def ease_in_quad(u: float) -> float:
"""
二次缓入(慢进快出)。
Parameters
----------
u : float
归一化时间,范围 [0, 1]。
Returns
-------
float
插值权重。
"""
return u * u
@staticmethod
def ease_out_quad(u: float) -> float:
"""
二次缓出(快进慢出)。
Parameters
----------
u : float
归一化时间,范围 [0, 1]。
Returns
-------
float
插值权重。
"""
return 1 - (1 - u) ** 2
@staticmethod
def ease_in_out_quad(u: float) -> float:
"""
二次缓入缓出。
Parameters
----------
u : float
归一化时间,范围 [0, 1]。
Returns
-------
float
插值权重。
"""
if u < 0.5:
return 2 * u * u
else:
return 1 - pow(-2 * u + 2, 2) / 2
@staticmethod
def hold(u: float) -> float:
"""
阶梯保持模式占位函数。实际插值逻辑在 ParamCurve.value_at 中特殊处理。
Parameters
----------
u : float
归一化时间(忽略)。
Returns
-------
float
无意义,仅作标识。
"""
return 0.0
@dataclass
class Keyframe:
"""
参数曲线上的一个关键帧,支持完整的入/出切线控制。
插值优先级:
1. 若 use_bezier=True → 使用贝塞尔模式(需 in_tangent / out_tangent
2. 否则 → 使用 out_interp 函数in_interp 被忽略)
"""
time: float
value: float
# 函数插值模式
out_interp: Optional[FittingFunctionType] = None
# 贝塞尔模式
in_tangent: Optional[Tuple[float, float]] = (
None # (dt, dv) ← 相对于自身(负 dt 表示左侧)
)
out_tangent: Optional[Tuple[float, float]] = (
None # (dt, dv) → 相对于自身(正 dt 表示右侧)
)
use_bezier: bool = False
class BoundaryBehaviour(str, Enum):
"""
边界行为枚举。
"""
CONSTANT = "constant"
"""返回默认基线值"""
HOLD = "hold"
"""保持首/尾关键帧的值"""
class ParamCurve:
"""
参数曲线类
"""
"""
支持动态节点编辑
用户通过添加/修改关键帧(时间-值对)来定义曲线,类自动在相邻关键帧之间生成插值段。
支持多种插值模式:线性('linear')、平滑缓动('smooth')、保持('hold')或自定义函数。
"""
base_line: float = 0.0
"""基线/默认值"""
base_interpolation_function: FittingFunctionType
"""默认(未指定区间时的)关键帧插值模式"""
boundary_behaviour: BoundaryBehaviour
"""边界行为,控制参数曲线在已定义的范围外的返回值"""
_keys: List[Keyframe]
"""关键帧列表"""
def __init__(
self,
base_value: float = 0.0,
default_interpolation_function: FittingFunctionType = InterpolationMethod.linear,
boundary_mode: BoundaryBehaviour = BoundaryBehaviour.CONSTANT,
):
"""
初始化参数曲线。
Parameters
----------
base_value : float
边界外默认值(当 boundary_mode 为 BoundaryBehaviour.CONSTANT 时使用)。
default_interp : FittingFunctionType
新关键帧的默认 out_interp。
boundary_mode : BoundaryBehaviour
范围外行为:
- BoundaryBehaviour.CONSTANT: 返回 base_value
- BoundaryBehaviour.HOLD: 保持首/尾关键帧值
"""
self.base_line = base_value
self.base_interpolation_function = default_interpolation_function
self.boundary_behaviour = boundary_mode
self._keys: List[Keyframe] = []
def add_key(
self,
time: float,
value: float,
out_interp: Optional[FittingFunctionType] = None,
in_tangent: Optional[Tuple[float, float]] = None,
out_tangent: Optional[Tuple[float, float]] = None,
use_bezier: bool = False,
):
"""
添加或更新关键帧。
Parameters
----------
time : float
关键帧时间。
value : float
参数值。
out_interp : Optional[Callable]
出插值函数(若 use_bezier=False
in_tangent : Optional[Tuple[float, float]]
入切线偏移 (dt, dv)。dt 通常为负(表示左侧),但存储为绝对偏移。
out_tangent : Optional[Tuple[float, float]]
出切线偏移 (dt, dv)。dt 通常为正。
use_bezier : bool
是否使用贝塞尔插值。
Returns
-------
None
Notes
-----
若时间已存在,更新该关键帧的所有属性。
"""
interp = (
out_interp if out_interp is not None else self.base_interpolation_function
)
new_key = Keyframe(time, value, interp, in_tangent, out_tangent, use_bezier)
idx = bisect.bisect_left(self._keys, time, key=lambda k: k.time)
if idx < len(self._keys) and self._keys[idx].time == time:
self._keys[idx] = new_key
else:
self._keys.insert(idx, new_key)
def remove_key(self, time: float):
"""
移除指定时间的关键帧。
Parameters
----------
time : float
要移除的关键帧时间。
Returns
-------
None
"""
idx = bisect.bisect_left(self._keys, time, key=lambda k: k.time)
if idx < len(self._keys) and self._keys[idx].time == time:
del self._keys[idx]
def update_key_value(self, time: float, new_value: float):
"""更新关键帧值,保留其他属性。"""
idx = bisect.bisect_left(self._keys, time, key=lambda k: k.time)
if idx < len(self._keys) and self._keys[idx].time == time:
k = self._keys[idx]
self._keys[idx] = Keyframe(
time, new_value, k.out_interp, k.in_tangent, k.out_tangent, k.use_bezier
)
def update_key_interp(
self,
time: float,
out_interp: Optional[FittingFunctionType] = None,
in_tangent: Optional[Tuple[float, float]] = None,
out_tangent: Optional[Tuple[float, float]] = None,
use_bezier: bool = False,
):
"""更新关键帧的插值属性。"""
idx = bisect.bisect_left(self._keys, time, key=lambda k: k.time)
if idx < len(self._keys) and self._keys[idx].time == time:
k = self._keys[idx]
new_value = k.value
interp = out_interp if out_interp is not None else k.out_interp
self._keys[idx] = Keyframe(
time, new_value, interp, in_tangent, out_tangent, use_bezier
)
def set_key_tangents(
self,
time: float,
in_tangent: Optional[Tuple[float, float]] = None,
out_tangent: Optional[Tuple[float, float]] = None,
use_bezier: bool = True,
):
"""单独设置关键帧的切线,不改变值。"""
idx = bisect.bisect_left(self._keys, time, key=lambda k: k.time)
if idx < len(self._keys) and self._keys[idx].time == time:
k = self._keys[idx]
self._keys[idx] = Keyframe(
time,
k.value,
out_interp=k.out_interp,
in_tangent=in_tangent,
out_tangent=out_tangent,
use_bezier=use_bezier,
)
def make_key_smooth(self, time: float):
"""
将关键帧设为“平滑”模式(自动对称切线,并设为贝塞尔模式)。
切线长度基于相邻关键帧的时间和值差。
"""
idx = bisect.bisect_left(self._keys, time, key=lambda k: k.time)
if idx < len(self._keys) and self._keys[idx].time == time:
k = self._keys[idx]
prev_k = self._keys[idx - 1] if idx > 0 else None
next_k = self._keys[idx + 1] if idx + 1 < len(self._keys) else None
# 默认切线长度:时间差的 1/3值差按比例
dt_in = dt_out = 0.1
dv_in = dv_out = 0.0
if prev_k and next_k:
dt_total = next_k.time - prev_k.time
dv_total = next_k.value - prev_k.value
dt_in = dt_out = dt_total / 3.0
dv_in = dv_out = dv_total / 3.0
elif prev_k:
dt_out = (k.time - prev_k.time) / 2.0
dv_out = (k.value - prev_k.value) / 2.0
dt_in = dt_out
dv_in = dv_out
elif next_k:
dt_in = (next_k.time - k.time) / 2.0
dv_in = (next_k.value - k.value) / 2.0
dt_out = dt_in
dv_out = dv_in
self.set_key_tangents(
time,
in_tangent=(-dt_in, -dv_in), # in_tangent 存储为偏移,使用时做减法
out_tangent=(dt_out, dv_out),
use_bezier=True,
)
def _get_boundary_value(self, t: float) -> float:
"""根据 boundary_mode 获取范围外的值。"""
if not self._keys:
return self.base_line
if self.boundary_behaviour == BoundaryBehaviour.CONSTANT:
return self.base_line
elif self.boundary_behaviour == BoundaryBehaviour.HOLD:
if t < self._keys[0].time:
return self._keys[0].value
else:
return self._keys[-1].value
else: # 可能会有别的模式吗?
return self.base_line
def value_at(self, t: float) -> float:
"""
计算时间 t 处的曲线值。
Parameters
----------
t : float
查询时间。
Returns
-------
float
插值结果。
"""
keys = self._keys
if not keys:
return self._get_boundary_value(t)
if t < keys[0].time or t > keys[-1].time:
return self._get_boundary_value(t)
times = [k.time for k in keys]
idx = bisect.bisect_right(times, t) - 1
if idx < 0:
return self._get_boundary_value(t)
if idx >= len(keys) - 1:
return keys[-1].value
k0 = keys[idx]
k1 = keys[idx + 1]
if k0.time == k1.time:
return k0.value
if k0.time == t:
return k0.value
if k1.time == t:
return k1.value
t0, v0 = k0.time, k0.value
t1, v1 = k1.time, k1.value
u = (t - t0) / (t1 - t0)
u = max(0.0, min(1.0, u))
# 贝塞尔模式(高优先级)
if k0.use_bezier or k1.use_bezier:
return _evaluate_bezier_segment(
t0,
v0,
t1,
v1,
out_tangent=k0.out_tangent,
in_tangent=k1.in_tangent, # ← 关键:使用下一帧的 in_tangent
u=u,
)
# 函数插值模式,优先处理阶梯保持模式
elif k0.out_interp is InterpolationMethod.hold:
return v0
interp_func = k0.out_interp or self.base_interpolation_function
v_norm = interp_func(u)
return v0 + v_norm * (v1 - v0)
def __call__(self, t: float) -> float:
return self.value_at(t)
def get_all_keys(self) -> List[Tuple[float, float]]:
"""返回 (time, value) 列表。"""
return [(k.time, k.value) for k in self._keys]
def set_default_interpolation_function(self, interp_func: FittingFunctionType):
"""设置默认插值函数。"""
self.base_interpolation_function = interp_func
def set_boundary_mode(
self, mode: BoundaryBehaviour, base_value: Optional[float] = None
):
"""
设置边界行为。
Parameters
----------
mode : BoundaryBehaviour
边界行为设定
base_value : Optional[float]
当 mode=BoundaryBehaviour.CONSTANT 时,指定新的默认值。
"""
self.boundary_behaviour = mode
if base_value is not None:
self.base_line = base_value
def bake(
self,
start: float,
end: float,
sample_rate: Optional[float] = None,
num_samples: Optional[int] = None,
dtype: Any = None,
) -> "np.ndarray": # type: ignore 这里这样用会报错吗?不知道,但是人工智能这样写了都,大抵是能用的吧
"""
将参数曲线在指定时间范围内烘焙为 NumPy 数组,用于高性能实时查询或音频渲染。
Parameters
----------
start : float
烘焙起始时间(包含)。
end : float
烘焙结束时间(不包含)。
sample_rate : Optional[float]
采样率(单位:样本/时间单位。例如若时间单位为秒sample_rate=48000 表示每秒 48k 样本。
必须与 `num_samples` 二选一提供。
num_samples : Optional[int]
输出数组的总样本数。若提供,则忽略 `sample_rate`。
dtype : Any, optional
输出数组的数据类型(如 np.float32。默认为 np.float64。
Returns
-------
np.ndarray
一维 NumPy 数组,长度为 `num_samples``arr[i] ≈ curve(start + i / sample_rate)`。
Exceptions
----------
ValueError
- 若 `start >= end`
- 若未提供 `sample_rate` 且未提供 `num_samples`
- 若 `num_samples <= 0`
Notes
-----
- 内部使用 `np.linspace` 生成时间轴,然后逐点调用 `self.value_at(t)`。
- 虽然目前是 Python 循环,但对于典型自动化曲线(<1000 关键帧NumPy 向量化优势主要体现在内存布局和后续处理。
- 如需极致性能(如 >1M 样本),可未来优化为 C++/Numba 加速,但当前已满足 DAW 自动化需求。
"""
if start >= end:
raise ValueError("起始值须小于结束值。")
if num_samples is not None:
if num_samples <= 0:
raise ValueError("烘焙的采样数须为非零自然数。")
n = num_samples
elif sample_rate is not None:
if sample_rate <= 0:
raise ValueError("烘焙的采样率须为正值。")
duration = end - start
n = int(ceil(duration * sample_rate))
# 别因为小数数值会产生的问题而越界了来着
if n == 0:
n = 1
else:
raise ValueError("烘焙参数时,须提供采样率或采样数。")
import numpy as np
# 生成对应时间的节点:[start, ..., end - dt]
times = np.linspace(start, end, n, endpoint=False)
# 计算每个时间节点上的参数值
# 我们认为在数字音频工作站的环境里,此值可能最多到 ~1e6 的样子,因此这样 for 一下应当可以接受
# WARNING: 人工智能是这样理解的,如果有问题的话后续可能需要更改
values = np.empty(n, dtype=dtype or np.float64)
for i in range(n):
values[i] = self.value_at(float(times[i]))
return values

View File

@@ -47,6 +47,7 @@
full = [
"TrimMCStruct <= 0.0.5.9",
"brotli >= 1.0.0",
"numpy"
]
dev = [
"TrimMCStruct <= 0.0.5.9",