@dsh-gis/dsh-video-studio-skill 0.9.1 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +473 -0
- package/README.md +263 -327
- package/THIRD_PARTY_NOTICES.md +2 -2
- package/package.json +9 -10
- package/schemas/design.schema.json +722 -0
- package/schemas/film.schema.json +31 -42
- package/schemas/proposal.schema.json +133 -0
- package/skills/video-studio/SKILL.md +1216 -1123
- package/skills/video-studio/assets/design_fixture.json +141 -0
- package/skills/video-studio/assets/film.json +2 -4
- package/skills/video-studio/assets/golden_frames.json +13 -10
- package/skills/video-studio/assets/scenes.py +153 -65
- package/skills/video-studio/assets/voice_loudness.json +16 -0
- package/skills/video-studio/references/appearance.md +123 -0
- package/skills/video-studio/references/audio-variation.md +74 -93
- package/skills/video-studio/references/audio.md +64 -66
- package/skills/video-studio/references/change-impact.md +5 -5
- package/skills/video-studio/references/{v08-quality.md → delivery-gates.md} +16 -18
- package/skills/video-studio/references/frame-qa.md +48 -16
- package/skills/video-studio/references/pipeline.md +4 -4
- package/skills/video-studio/references/pitfalls.md +22 -3
- package/skills/video-studio/references/proposal.md +160 -0
- package/skills/video-studio/references/quality-gates.md +11 -11
- package/skills/video-studio/references/scene-authoring.md +2 -2
- package/skills/video-studio/references/scene-grammar.md +40 -42
- package/skills/video-studio/references/sfx-design.md +8 -6
- package/skills/video-studio/references/web-engine.md +8 -8
- package/skills/video-studio/references/word-timeline.md +9 -9
- package/skills/video-studio/requirements.txt +9 -13
- package/skills/video-studio/scripts/designkit.py +2115 -0
- package/skills/video-studio/scripts/filmcheck.py +22 -48
- package/skills/video-studio/scripts/filmkit.py +2980 -2904
- package/skills/video-studio/scripts/gates.py +4 -4
- package/skills/video-studio/scripts/grammar.py +73 -13
- package/skills/video-studio/scripts/musicgen.py +2042 -0
- package/skills/video-studio/scripts/preflight.py +14 -7
- package/skills/video-studio/scripts/previewkit.py +848 -0
- package/skills/video-studio/scripts/propose.py +825 -0
- package/skills/video-studio/scripts/qa_frames.py +73 -39
- package/skills/video-studio/scripts/qaasr.py +26 -7
- package/skills/video-studio/scripts/qavmaf.py +89 -23
- package/skills/video-studio/scripts/render.py +545 -231
- package/skills/video-studio/scripts/scorekit.py +55 -1755
- package/skills/video-studio/scripts/selfcheck.py +815 -215
- package/skills/video-studio/scripts/sfxkit.py +27 -26
- package/skills/video-studio/scripts/stagewatch.py +24 -8
- package/skills/video-studio/scripts/stylekit.py +207 -888
- package/skills/video-studio/scripts/voicekit.py +508 -387
- package/skills/video-studio/scripts/webengine.py +71 -72
- package/tools/audio_similarity.py +64 -37
- package/tools/ci_check.py +20 -14
- package/tools/estimate.py +34 -29
- package/tools/golden_frames.py +72 -28
- package/tools/silhouette_qa.py +520 -168
- package/docs/demo-pil-sheet.png +0 -0
- package/docs/styles/demo-results.json +0 -76
- package/docs/styles/sheet-8-styles.png +0 -0
- package/docs/styles/sheet-ai.png +0 -0
- package/docs/styles/sheet-before-after.png +0 -0
- package/docs/styles/sheet-craft.png +0 -0
- package/docs/styles/sheet-night.png +0 -0
- package/skills/video-studio/assets/score_demo.json +0 -307
- package/skills/video-studio/references/style-packs.md +0 -283
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
|
-
"""scorekit.py —
|
|
2
|
+
"""scorekit.py — 程序化配乐。
|
|
3
3
|
|
|
4
4
|
用代码写一张能贯穿整部片子的配乐:调性/和弦表、段落能量、节拍网格、律动层、
|
|
5
5
|
段落过门、自动电平与"吸气",全部由 numpy + sfxkit 现场合成,不用任何采样。
|
|
@@ -22,29 +22,16 @@
|
|
|
22
22
|
render(duration, bpm=120, key="Dm", anchors=None, sections=None, seed=7) -> (samples, report)
|
|
23
23
|
save(path, x, sr=SR) / load(path, sr=SR) / selftest()
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
spectral_centroid(x, sr=SR) 频谱质心(量"亮度",验证风格差异用)
|
|
36
|
-
|
|
37
|
-
v0.7 新增(内容指纹变奏,references/audio-variation.md §3.3):
|
|
38
|
-
fingerprint 不再只派生音色 seed —— 同一风格换内容指纹时,结构层也稳定地变:
|
|
39
|
-
* 进行档:按指纹从 style["progressions"] 的 calm/body/drive 里另选一档
|
|
40
|
-
(整片一个偏移、climax 仍复用 drive,片内各段的进行关系保持连贯);
|
|
41
|
-
* 和弦:每两小节一次稳定替换(i<->i7 / VI<->VI7 / III<->III7 / VII<->VII7;
|
|
42
|
-
大调侧 I<->Imaj7 / IV<->IVmaj7),不覆盖乐句末属和弦与片尾主和弦;
|
|
43
|
-
* 动机:每个 (拍, 级数, 时值) 做 ±1 级数偏移(只改级数不改节奏;问句/答句/
|
|
44
|
-
片尾三种动机独立派生,跨主音的上下行用八度偏移修正);
|
|
45
|
-
* 鼓型:kick 最多加一记 ghost 落点(0.5 拍网格、不与现有落点重复),clap ±0.5 拍。
|
|
46
|
-
report["variation"] = {"chord_swaps", "motif_shifts", "drum_ghosts"} 记实际发生数。
|
|
47
|
-
style=None 的 legacy 路径不经过这些代码,仍然逐字节兼容(selfcheck 20 条)。
|
|
25
|
+
零模板:本模块不提供曲风表,只提供**技术原语**——配乐的调式/调性/BPM/情绪/编曲请用
|
|
26
|
+
musicgen.py(参数化编排器)现场编排:
|
|
27
|
+
* Score:和声表 / 段落能量 / 节拍网格 / 律动层 / 过门 / 吸气 / 自动电平;
|
|
28
|
+
* _Bus / _Bank:总线、混音、六类合成音色、乒乓延迟、侧链;
|
|
29
|
+
* _k_weighted_mono / _approx_lufs / _short_term_db:响度与频谱测量;
|
|
30
|
+
* save / load:24-bit PCM WAV 读写(NaN/Inf 拒绝写盘);
|
|
31
|
+
* render(duration, bpm, key, anchors, sections, seed):确定性配乐路径,
|
|
32
|
+
逐样本可复现(自测第 18 条守着)。
|
|
33
|
+
render() 仍接受 style/mood/fingerprint/music_plan/pace 这几个参数,只是为了
|
|
34
|
+
让既有调用方不因 TypeError 静默崩掉;非 legacy 的 style 一律明确报错并指向 musicgen.py。
|
|
48
35
|
"""
|
|
49
36
|
from __future__ import annotations
|
|
50
37
|
|
|
@@ -62,7 +49,7 @@ import numpy as np
|
|
|
62
49
|
|
|
63
50
|
import sfxkit as S # 唯一音频依赖:numpy + sfxkit
|
|
64
51
|
|
|
65
|
-
__version__ = "0.
|
|
52
|
+
__version__ = "1.0.4"
|
|
66
53
|
|
|
67
54
|
SR = 48000 # 采样率(与 filmkit / sfxkit 一致)
|
|
68
55
|
BEATS_PER_BAR = 4 # 4/4
|
|
@@ -126,7 +113,7 @@ _DEGREE_TABLE = (
|
|
|
126
113
|
("VII", 10, (0, 4, 7, 12), ""),
|
|
127
114
|
("I", 0, (0, 4, 7, 12), ""), # 结尾大三和弦(皮卡迪三度)
|
|
128
115
|
("sus2", 0, (0, 2, 7, 12), "sus2"), # 悬留色彩,过门/段首可用
|
|
129
|
-
# ----
|
|
116
|
+
# ---- 大调级数与七和弦色彩。只允许在表尾追加;前 10 项顺序不许动(_build_key 依赖) ----
|
|
130
117
|
("ii", 2, (0, 3, 7, 12), "m"),
|
|
131
118
|
("iii", 4, (0, 3, 7, 12), "m"),
|
|
132
119
|
("IV", 5, (0, 4, 7, 12), ""),
|
|
@@ -140,7 +127,7 @@ _DEGREE_TABLE = (
|
|
|
140
127
|
("V7", 7, (0, 4, 7, 10), "7"),
|
|
141
128
|
("vi7", 9, (0, 3, 7, 10), "m7"),
|
|
142
129
|
("i7", 0, (0, 3, 7, 10), "m7"),
|
|
143
|
-
# ----
|
|
130
|
+
# ---- 和弦替换用的七和弦色彩。同根音、同三音,只把五度后的音换成小七度;
|
|
144
131
|
# 只允许在表尾追加(前 10 项顺序被 _build_key / 老报告依赖),供 _CHORD_SWAPS 使用。 ----
|
|
145
132
|
("VI7", 8, (0, 4, 7, 10), "7"),
|
|
146
133
|
("III7", 3, (0, 4, 7, 10), "7"),
|
|
@@ -251,14 +238,14 @@ KEYS = {
|
|
|
251
238
|
"Em": _build_key("E", 4, "sharp", "E 小调"),
|
|
252
239
|
}
|
|
253
240
|
|
|
254
|
-
#
|
|
241
|
+
# 大调三档和声进行。小调那边 PROGRESSIONS 保持原样:legacy 路径只读 PROGRESSIONS。
|
|
255
242
|
PROGRESSIONS_MAJOR = {
|
|
256
243
|
"calm": ("I", "vi", "IV", "V"),
|
|
257
244
|
"body": ("I", "V", "vi", "IV"),
|
|
258
245
|
"drive": ("I", "IV", "V", "I"),
|
|
259
246
|
}
|
|
260
247
|
|
|
261
|
-
#
|
|
248
|
+
# 6 个大调(与 stylekit.MAJOR_KEYS 同名同义)。用同一个 _build_key 构建,
|
|
262
249
|
# 语义与小调键完全一致(级数表 -> 和弦名/低音/音名);只是 progression 换成大调级数。
|
|
263
250
|
# 写法按惯例挑:C/F/Bb 用降号、G/D/A 用升号。
|
|
264
251
|
KEYS_MAJOR = {
|
|
@@ -273,7 +260,7 @@ for _kname in KEYS_MAJOR:
|
|
|
273
260
|
KEYS_MAJOR[_kname]["progression"] = dict(PROGRESSIONS_MAJOR)
|
|
274
261
|
del _kname
|
|
275
262
|
|
|
276
|
-
# 全部可用调:4 个小调 + 6 个大调。KEYS 本身保持
|
|
263
|
+
# 全部可用调:4 个小调 + 6 个大调。KEYS 本身保持 4 个小调不变(兼容性锚点)。
|
|
277
264
|
KEYS_ALL = dict(KEYS)
|
|
278
265
|
KEYS_ALL.update(KEYS_MAJOR)
|
|
279
266
|
|
|
@@ -645,209 +632,6 @@ class _Bank:
|
|
|
645
632
|
return tex
|
|
646
633
|
|
|
647
634
|
|
|
648
|
-
# ---------------- v0.5 风格化音色(只用已有原语组合;不碰老乐器的 RNG 序列) ----------------
|
|
649
|
-
#
|
|
650
|
-
# 三个约定,保证 legacy 逐样本不变:
|
|
651
|
-
# 1) 每个新音色都走 self._cached(...) / self._cache,种子用 self._seed("<新名字>", ...),
|
|
652
|
-
# 名字前缀与老乐器不重叠,老乐器一次都不读新缓存;
|
|
653
|
-
# 2) 不在 __init__ 里预渲染任何新素材(既有 rng 调用次序不变);
|
|
654
|
-
# 3) 新方法只在 render_styled 里被调用。
|
|
655
|
-
def strings(self, chord: dict, bright: float, attack: float, release: float = 0.7,
|
|
656
|
-
octave: int = 0) -> np.ndarray:
|
|
657
|
-
"""弦乐铺底:5 声部超级锯齿(detune 0.06)过低通,比合成器 pad 更宽、更"有机"。"""
|
|
658
|
-
key = ("strings", chord["name"], int(round(bright * 10)),
|
|
659
|
-
int(round(attack * 100)), int(octave))
|
|
660
|
-
hit = self._cache.get(key)
|
|
661
|
-
if hit is not None:
|
|
662
|
-
return hit
|
|
663
|
-
core = self._strings_core(chord, bright, octave)
|
|
664
|
-
env = self._pad_env(attack, release)
|
|
665
|
-
tex = self._edge_fade(_f32(core * env[:, None]), 0.002, 0.02)
|
|
666
|
-
self._cache[key] = tex
|
|
667
|
-
return tex
|
|
668
|
-
|
|
669
|
-
def _strings_core(self, chord: dict, bright: float, octave: int) -> np.ndarray:
|
|
670
|
-
key = ("strcore", chord["name"], int(round(bright * 10)), int(octave))
|
|
671
|
-
hit = self._str_cores.get(key)
|
|
672
|
-
if hit is not None:
|
|
673
|
-
return hit
|
|
674
|
-
names = [_up(nt, octave) for nt in chord["notes"]] if octave else list(chord["notes"])
|
|
675
|
-
n = self._pad_n
|
|
676
|
-
out = np.zeros((n, 2), np.float32)
|
|
677
|
-
for c in range(2):
|
|
678
|
-
g = S.rng(self._seed("strings", chord["name"], int(round(bright * 10)), octave, c))
|
|
679
|
-
acc = np.zeros(n, np.float64)
|
|
680
|
-
for nt in names:
|
|
681
|
-
acc += S.supersaw(S.hz(nt), n, voices=5, detune=0.06, seed=g, sr=self.sr)
|
|
682
|
-
acc = S.lowpass(acc, 420.0 + 2600.0 * bright, 0.8, sr=self.sr)
|
|
683
|
-
acc = S.highpass(acc, 70.0, sr=self.sr)
|
|
684
|
-
out[:, c] = _f32(acc)
|
|
685
|
-
out *= np.float32(1.9 / max(1, len(names)))
|
|
686
|
-
self._str_cores[key] = out
|
|
687
|
-
return out
|
|
688
|
-
|
|
689
|
-
def cello(self, note: str, dur: float, bright: float) -> np.ndarray:
|
|
690
|
-
"""大提琴式低音:锯齿 + 脉冲、带一点揉弦,慢起音。"""
|
|
691
|
-
key = ("cello", note, round(float(dur), 3), int(round(bright * 10)))
|
|
692
|
-
return self._cached(key, lambda: self._make_cello(note, dur, bright))
|
|
693
|
-
|
|
694
|
-
def _make_cello(self, note: str, dur: float, bright: float) -> np.ndarray:
|
|
695
|
-
f = S.hz(note)
|
|
696
|
-
n = _ns(dur + 0.06, self.sr)
|
|
697
|
-
t = _t(n, self.sr)
|
|
698
|
-
vib = 1.0 + 0.0045 * np.sin(2.0 * np.pi * 5.1 * t)
|
|
699
|
-
osc = (0.70 * S.saw(f * vib, n, sr=self.sr)
|
|
700
|
-
+ 0.35 * S.pulse(f * vib * 1.001, n, 0.42, sr=self.sr))
|
|
701
|
-
x = S.lowpass(osc, 260.0 + 1500.0 * bright, 1.1, sr=self.sr)
|
|
702
|
-
env = np.minimum(1.0, t / 0.025) * np.clip((dur + 0.05 - t) / 0.06, 0.0, 1.0)
|
|
703
|
-
x = np.tanh(x * 1.6) * 0.5 * env
|
|
704
|
-
return self._edge_fade(_f32(x), 0.002, 0.01)
|
|
705
|
-
|
|
706
|
-
def sub_bass(self, note: str, dur: float) -> np.ndarray:
|
|
707
|
-
"""808 式超低音:正弦轻微下滑 + tanh 饱和(lo-fi / 电子底的"地基")。"""
|
|
708
|
-
key = ("subbass", note, round(float(dur), 3))
|
|
709
|
-
return self._cached(key, lambda: self._make_sub_bass(note, dur))
|
|
710
|
-
|
|
711
|
-
def _make_sub_bass(self, note: str, dur: float) -> np.ndarray:
|
|
712
|
-
f = S.hz(note)
|
|
713
|
-
d = float(min(1.2, max(0.08, dur)))
|
|
714
|
-
n = _ns(d + 0.08, self.sr)
|
|
715
|
-
t = _t(n, self.sr)
|
|
716
|
-
fr = f + (f * 1.03 - f) * np.exp(-t / 0.04)
|
|
717
|
-
x = S.sine(fr, n, sr=self.sr) * np.exp(-t / max(0.08, d * 0.55))
|
|
718
|
-
x = np.tanh(x * 1.8) * 0.8 * np.minimum(1.0, t / 0.003)
|
|
719
|
-
return self._edge_fade(_f32(x), 0.001, 0.01)
|
|
720
|
-
|
|
721
|
-
def pulse_bass(self, note: str, dur: float, bright: float) -> np.ndarray:
|
|
722
|
-
"""脉冲贝斯:方波为主 + 时变低通,比老 bass 更"电子"。"""
|
|
723
|
-
key = ("pulsebass", note, round(float(dur), 3), int(round(bright * 10)))
|
|
724
|
-
return self._cached(key, lambda: self._make_pulse_bass(note, dur, bright))
|
|
725
|
-
|
|
726
|
-
def _make_pulse_bass(self, note: str, dur: float, bright: float) -> np.ndarray:
|
|
727
|
-
f = S.hz(note)
|
|
728
|
-
n = _ns(dur + 0.07, self.sr)
|
|
729
|
-
t = _t(n, self.sr)
|
|
730
|
-
osc = (0.80 * S.pulse(f, n, 0.34, sr=self.sr)
|
|
731
|
-
+ 0.35 * S.saw(f * 1.002, n, sr=self.sr))
|
|
732
|
-
cut = 190.0 + (420.0 + 1700.0 * bright) * np.exp(-t / 0.1)
|
|
733
|
-
x = S.tv_filter(osc, "lowpass", cut, 1.0, block=64, sr=self.sr)
|
|
734
|
-
env = np.minimum(1.0, t / 0.004) * np.clip((dur + 0.05 - t) / 0.05, 0.0, 1.0)
|
|
735
|
-
x = np.tanh(x * 1.7) * 0.55 * env
|
|
736
|
-
return self._edge_fade(_f32(x), 0.001, 0.005)
|
|
737
|
-
|
|
738
|
-
def pluck_note(self, note: str, dur: float, bright: float) -> np.ndarray:
|
|
739
|
-
"""Karplus-Strong 拨弦(sfxkit.pluck)—— 民谣 / 手作的木头感。"""
|
|
740
|
-
key = ("plucknote", note, round(float(dur), 3), int(round(bright * 10)))
|
|
741
|
-
return self._cached(key, lambda: self._make_pluck(note, dur, bright))
|
|
742
|
-
|
|
743
|
-
def _make_pluck(self, note: str, dur: float, bright: float) -> np.ndarray:
|
|
744
|
-
f = S.hz(note)
|
|
745
|
-
d = float(min(0.9, max(0.12, dur)))
|
|
746
|
-
x = S.pluck(f, d, brightness=_clip01(0.35 + 0.55 * bright), decay=0.9962,
|
|
747
|
-
seed=self._seed("pluck", note, int(round(bright * 10))), sr=self.sr)
|
|
748
|
-
x = np.asarray(x, dtype=np.float64)
|
|
749
|
-
want = _ns(d, self.sr)
|
|
750
|
-
if len(x) < want:
|
|
751
|
-
x = np.pad(x, (0, want - len(x)))
|
|
752
|
-
return self._edge_fade(_f32(x * 0.9), 0.0005, 0.01)
|
|
753
|
-
|
|
754
|
-
def epiano(self, note: str, dur: float) -> np.ndarray:
|
|
755
|
-
"""电钢:FM 钟的变体 + 低八度正弦托底(杂志 / lo-fi 的"暖")。"""
|
|
756
|
-
key = ("epiano", note, round(float(dur), 3))
|
|
757
|
-
return self._cached(key, lambda: self._make_epiano(note, dur))
|
|
758
|
-
|
|
759
|
-
def _make_epiano(self, note: str, dur: float) -> np.ndarray:
|
|
760
|
-
f = S.hz(note)
|
|
761
|
-
d = float(min(3.0, max(0.25, dur)))
|
|
762
|
-
n = _ns(d, self.sr)
|
|
763
|
-
t = _t(n, self.sr)
|
|
764
|
-
idx = 1.5 * np.exp(-t / 0.12)
|
|
765
|
-
x = S.fm(f, 2.0, idx, n, sr=self.sr) * np.exp(-t / (d * 0.45))
|
|
766
|
-
x += 0.42 * S.sine(f * 0.5, n, sr=self.sr) * np.exp(-t / (d * 0.7))
|
|
767
|
-
x += 0.12 * S.fm(f * 2.0, 1.0, 0.35, n, sr=self.sr) * np.exp(-t / 0.25)
|
|
768
|
-
x *= np.minimum(1.0, t / 0.003)
|
|
769
|
-
return self._edge_fade(_f32(x * 0.6), 0.001, 0.02)
|
|
770
|
-
|
|
771
|
-
def marimba(self, note: str, dur: float) -> np.ndarray:
|
|
772
|
-
"""马林巴:正弦基音 + 很短的 FM 敲击泛音。"""
|
|
773
|
-
key = ("marimba", note, round(float(dur), 3))
|
|
774
|
-
return self._cached(key, lambda: self._make_marimba(note, dur))
|
|
775
|
-
|
|
776
|
-
def _make_marimba(self, note: str, dur: float) -> np.ndarray:
|
|
777
|
-
f = S.hz(note)
|
|
778
|
-
d = float(min(2.0, max(0.35, dur)))
|
|
779
|
-
n = _ns(d, self.sr)
|
|
780
|
-
t = _t(n, self.sr)
|
|
781
|
-
x = S.sine(f, n, sr=self.sr) * np.exp(-t / 0.30)
|
|
782
|
-
x += 0.45 * S.fm(f, 3.0, 1.1 * np.exp(-t / 0.02), n, sr=self.sr) * np.exp(-t / 0.06)
|
|
783
|
-
x += 0.20 * S.sine(4.0 * f, n, sr=self.sr) * np.exp(-t / 0.05)
|
|
784
|
-
x *= np.minimum(1.0, t / 0.0015)
|
|
785
|
-
return self._edge_fade(_f32(x * 0.7), 0.0008, 0.02)
|
|
786
|
-
|
|
787
|
-
def glock(self, note: str, dur: float = 1.0) -> np.ndarray:
|
|
788
|
-
"""钟琴:高比率 FM + 非谐波正弦,短促闪亮(vivid 的"活力")。"""
|
|
789
|
-
key = ("glock", note, round(float(dur), 2))
|
|
790
|
-
return self._cached(key, lambda: self._make_glock(note, dur))
|
|
791
|
-
|
|
792
|
-
def _make_glock(self, note: str, dur: float) -> np.ndarray:
|
|
793
|
-
f = S.hz(note)
|
|
794
|
-
d = float(min(1.6, max(0.25, dur)))
|
|
795
|
-
n = _ns(d, self.sr)
|
|
796
|
-
t = _t(n, self.sr)
|
|
797
|
-
x = S.fm(f, 3.5, 1.8 * np.exp(-t / 0.09), n, sr=self.sr) * np.exp(-t / 0.28)
|
|
798
|
-
x += 0.30 * S.sine(2.76 * f, n, sr=self.sr) * np.exp(-t / 0.16)
|
|
799
|
-
x *= np.minimum(1.0, t / 0.001)
|
|
800
|
-
return self._edge_fade(_f32(x * 0.55), 0.0005, 0.02)
|
|
801
|
-
|
|
802
|
-
def strings_bell(self, note: str, dur: float) -> np.ndarray:
|
|
803
|
-
"""弦乐 + 钟的混合主奏:有弓弦的持续感,又有钟的起音(cinema / magazine)。"""
|
|
804
|
-
key = ("strbell", note, round(float(dur), 2))
|
|
805
|
-
return self._cached(key, lambda: self._make_strbell(note, dur))
|
|
806
|
-
|
|
807
|
-
def _make_strbell(self, note: str, dur: float) -> np.ndarray:
|
|
808
|
-
f = S.hz(note)
|
|
809
|
-
d = float(min(3.4, max(0.8, dur)))
|
|
810
|
-
n = _ns(d, self.sr)
|
|
811
|
-
t = _t(n, self.sr)
|
|
812
|
-
g = S.rng(self._seed("strbell", note, round(d, 2)))
|
|
813
|
-
osc = S.supersaw(f, n, voices=3, detune=0.05, seed=g, sr=self.sr)
|
|
814
|
-
osc = S.lowpass(osc, 1500.0, 0.8, sr=self.sr) * np.exp(-t / (d * 0.55))
|
|
815
|
-
bell = S.fm(f, 3.5, 1.6 * np.exp(-t / 0.3), n, sr=self.sr) * np.exp(-t / (d * 0.4))
|
|
816
|
-
x = (0.55 * osc + 0.85 * bell) * np.minimum(1.0, t / 0.004)
|
|
817
|
-
return self._edge_fade(_f32(x * 0.6), 0.002, 0.03)
|
|
818
|
-
|
|
819
|
-
def brass(self, note: str, dur: float, bright: float) -> np.ndarray:
|
|
820
|
-
"""铜管:锯齿 + 脉冲过带通,起音短促(epic 的"定格重音")。"""
|
|
821
|
-
key = ("brass", note, round(float(dur), 3), int(round(bright * 10)))
|
|
822
|
-
return self._cached(key, lambda: self._make_brass(note, dur, bright))
|
|
823
|
-
|
|
824
|
-
def _make_brass(self, note: str, dur: float, bright: float) -> np.ndarray:
|
|
825
|
-
f = S.hz(note)
|
|
826
|
-
n = _ns(dur + 0.06, self.sr)
|
|
827
|
-
t = _t(n, self.sr)
|
|
828
|
-
osc = (0.85 * S.saw(f, n, sr=self.sr)
|
|
829
|
-
+ 0.40 * S.pulse(f * 1.001, n, 0.42, sr=self.sr))
|
|
830
|
-
x = S.tv_filter(osc, "lowpass", 900.0 + 2600.0 * bright, 1.2, block=64, sr=self.sr)
|
|
831
|
-
x = S.bandpass(x, 900.0 + 1200.0 * bright, 0.8, sr=self.sr)
|
|
832
|
-
env = np.minimum(1.0, t / 0.035) * np.clip((dur + 0.04 - t) / 0.06, 0.0, 1.0)
|
|
833
|
-
x = np.tanh(x * 1.5) * 0.5 * env
|
|
834
|
-
return self._edge_fade(_f32(x), 0.002, 0.01)
|
|
835
|
-
|
|
836
|
-
def stab(self, note: str, dur: float, bright: float) -> np.ndarray:
|
|
837
|
-
"""和弦短切音:极短带通脉冲,琶音层用它做"打点"。"""
|
|
838
|
-
key = ("stab", note, round(float(dur), 3), int(round(bright * 10)))
|
|
839
|
-
return self._cached(key, lambda: self._make_stab(note, dur, bright))
|
|
840
|
-
|
|
841
|
-
def _make_stab(self, note: str, dur: float, bright: float) -> np.ndarray:
|
|
842
|
-
f = S.hz(note)
|
|
843
|
-
d = float(min(0.6, max(0.12, dur)))
|
|
844
|
-
n = _ns(d, self.sr)
|
|
845
|
-
t = _t(n, self.sr)
|
|
846
|
-
osc = (0.70 * S.pulse(f, n, 0.30, sr=self.sr)
|
|
847
|
-
+ 0.50 * S.saw(f * 1.003, n, sr=self.sr))
|
|
848
|
-
x = S.bandpass(osc, 1000.0 + 2400.0 * bright, 1.1, sr=self.sr) * np.exp(-t / 0.06)
|
|
849
|
-
x *= np.minimum(1.0, t / 0.001)
|
|
850
|
-
return self._edge_fade(_f32(x * 0.8), 0.0005, 0.008)
|
|
851
635
|
|
|
852
636
|
# ---------------- 效果素材 ----------------
|
|
853
637
|
def sub_boom(self, dur: float = 2.2, f0: float = 55.0, f1: float = 30.0) -> np.ndarray:
|
|
@@ -1646,1249 +1430,6 @@ class Score:
|
|
|
1646
1430
|
|
|
1647
1431
|
|
|
1648
1432
|
|
|
1649
|
-
# ============================================================================
|
|
1650
|
-
# v0.5 配乐风格层(MUSIC_STYLES):一个 style id = 一种"配乐性格"
|
|
1651
|
-
# ============================================================================
|
|
1652
|
-
#
|
|
1653
|
-
# 为什么要这一层:v0.4 的配乐只有一套和声进行 / 一套动机 / 一张 energy 查表,
|
|
1654
|
-
# 所以「不管什么内容,听众听到的都是同一首曲子的不同时长」。v0.5 把"写什么"拆成
|
|
1655
|
-
# 风格包:和弦进行、动机、鼓型、音色、空间、动态各自成表,同一部片的配乐由 style 决定。
|
|
1656
|
-
#
|
|
1657
|
-
# 与 stylekit.STYLES 的关系:
|
|
1658
|
-
# * 本模块不 import stylekit(scorekit 要能单独拷走运行,且 stylekit 是本模块的上游);
|
|
1659
|
-
# style id 与调池手工对齐,selftest 在 stylekit 存在时会交叉核对(缺失则 skip);
|
|
1660
|
-
# * stylekit 管"策展层"(BPM 梯度 / 调池 / 情绪),这里管"合成层"(和弦 / 鼓型 / 音色 /
|
|
1661
|
-
# 空间 / 动态),两边用同一个 style id 对齐。
|
|
1662
|
-
#
|
|
1663
|
-
# 数据契约(每个风格一个纯数据 dict,无 RNG 副作用):
|
|
1664
|
-
# bpm_hint 仅供文档 / CLI 显示;真正 BPM 由 stylekit 决定后传给 render_styled
|
|
1665
|
-
# keys 调池(与 STYLES[sid]["music"]["keys"] 一致;不在池里的调也能渲,只是不"正宗")
|
|
1666
|
-
# progressions {"calm"/"body"/"drive": 级数元组},级数必须在 _DEGREE_TABLE 里
|
|
1667
|
-
# motif {"q"/"a"/"end": ((拍, 级数, 时值拍), ...)}:级数相对主音,供主奏轮换
|
|
1668
|
-
# layers 层 -> 最低能量(0~1);过线才出现,进出各花 1 小节渐入渐出
|
|
1669
|
-
# voices {"pad"/"bass"/"arp"/"lead": 音色名},可选值见 _STYLE_VOICES
|
|
1670
|
-
# lead_cycle 主奏音色轮换表(每 phrase_bars 轮换一个)
|
|
1671
|
-
# drums {"calm"/"body"/"drive"/"climax": {kick,hat,clap,bass,arp}}
|
|
1672
|
-
# space {"delay_beats","delay_db","taps"} 乒乓延迟(拍为单位)
|
|
1673
|
-
# phrase_bars 4 / 8
|
|
1674
|
-
# duck {"bass","pad"} 侧链深度
|
|
1675
|
-
# level_span_db 段落能量 -> 电平的跨度(dB);8.5 与 legacy 的 energy_target_db 同口径
|
|
1676
|
-
# swing 0.0 / 0.08~0.16:奇数列踩镲与琶音延后(拍),只改位置不改 RNG 调用次数
|
|
1677
|
-
|
|
1678
|
-
MUSIC_STYLES = {
|
|
1679
|
-
# 科技深蓝:电子、切分、拨弦琶音;小调,四四拍推进
|
|
1680
|
-
"tech-deep": {
|
|
1681
|
-
"label": "科技深蓝",
|
|
1682
|
-
"bpm_hint": (104, 112, 120),
|
|
1683
|
-
"keys": ("Dm", "Am", "Em", "Cm"),
|
|
1684
|
-
"progressions": {
|
|
1685
|
-
"calm": ("i", "VI", "iv", "v"),
|
|
1686
|
-
"body": ("i", "VI", "III", "VII"),
|
|
1687
|
-
"drive": ("i", "VII", "VI", "V"),
|
|
1688
|
-
},
|
|
1689
|
-
"motif": {
|
|
1690
|
-
"q": ((0.0, "v", 0.75), (1.0, "i", 0.75), (2.0, "III", 0.75), (3.0, "iv", 1.75),
|
|
1691
|
-
(5.0, "III", 0.75), (6.0, "v", 0.75), (7.0, "i", 1.5)),
|
|
1692
|
-
"a": ((0.0, "v", 0.75), (1.0, "i", 0.75), (2.0, "III", 0.75), (3.0, "v", 0.75),
|
|
1693
|
-
(4.0, "VII", 2.0)),
|
|
1694
|
-
"end": ((0.0, "v", 1.0), (1.5, "i", 1.0), (2.5, "III", 1.5), (4.0, "i", 3.0)),
|
|
1695
|
-
},
|
|
1696
|
-
"layers": {"pad": 0.0, "bass": 0.24, "kick": 0.36, "hat": 0.46,
|
|
1697
|
-
"arp": 0.54, "melody": 0.66, "shine": 0.86},
|
|
1698
|
-
"voices": {"pad": "pad_saw", "bass": "pulse", "arp": "pluck", "lead": "bell"},
|
|
1699
|
-
"lead_cycle": ("bell", "marimba"),
|
|
1700
|
-
"drums": {
|
|
1701
|
-
"calm": {"kick": (), "hat": None, "clap": (), "bass": "whole", "arp": None},
|
|
1702
|
-
"body": {"kick": (0.0, 2.0), "hat": 1.0, "clap": (), "bass": "half", "arp": 0.5},
|
|
1703
|
-
"drive": {"kick": (0.0, 1.0, 2.0, 3.0), "hat": 0.5, "clap": (1.0, 3.0),
|
|
1704
|
-
"bass": "off8", "arp": 0.25},
|
|
1705
|
-
"climax": {"kick": (0.0, 1.0, 2.0, 3.0), "hat": 0.25, "clap": (1.0, 3.0),
|
|
1706
|
-
"bass": "16", "arp": 0.25},
|
|
1707
|
-
},
|
|
1708
|
-
"space": {"delay_beats": 0.75, "delay_db": -15.0, "taps": 3},
|
|
1709
|
-
"phrase_bars": 4,
|
|
1710
|
-
"duck": {"bass": 0.44, "pad": 0.30},
|
|
1711
|
-
"level_span_db": 8.5,
|
|
1712
|
-
"swing": 0.0,
|
|
1713
|
-
},
|
|
1714
|
-
# 杂志暖白:大调七和弦、电钢、大提琴贝斯;慢速、留白多
|
|
1715
|
-
"magazine": {
|
|
1716
|
-
"label": "杂志暖白",
|
|
1717
|
-
"bpm_hint": (88, 92, 96, 100),
|
|
1718
|
-
"keys": ("C", "G", "D", "F"),
|
|
1719
|
-
"progressions": {
|
|
1720
|
-
"calm": ("Imaj7", "vi7", "ii7", "V7"),
|
|
1721
|
-
"body": ("I", "vi", "IV", "V"),
|
|
1722
|
-
"drive": ("I", "IV", "V", "vi"),
|
|
1723
|
-
},
|
|
1724
|
-
"motif": {
|
|
1725
|
-
"q": ((0.0, "V", 0.9), (0.9, "VI", 0.9), (1.8, "V", 0.9), (2.7, "III", 0.9),
|
|
1726
|
-
(3.5, "I", 1.5), (5.0, "ii", 0.9), (5.9, "III", 0.9), (6.8, "V", 2.2)),
|
|
1727
|
-
"a": ((0.0, "V", 0.9), (0.9, "VI", 0.9), (1.8, "iii", 0.9), (2.7, "ii", 0.9),
|
|
1728
|
-
(3.5, "I", 1.5), (5.0, "III", 0.9), (6.0, "V", 2.4)),
|
|
1729
|
-
"end": ((0.0, "V", 1.2), (1.5, "VI", 0.9), (2.5, "V", 1.0), (3.5, "I", 3.0)),
|
|
1730
|
-
},
|
|
1731
|
-
"layers": {"pad": 0.0, "bass": 0.30, "kick": 0.42, "hat": 0.52,
|
|
1732
|
-
"arp": 0.50, "melody": 0.30, "shine": 0.90},
|
|
1733
|
-
"voices": {"pad": "strings", "bass": "cello", "arp": "epiano", "lead": "celesta"},
|
|
1734
|
-
"lead_cycle": ("celesta", "strings_bell"),
|
|
1735
|
-
"drums": {
|
|
1736
|
-
"calm": {"kick": (), "hat": None, "clap": (), "bass": "whole", "arp": None},
|
|
1737
|
-
"body": {"kick": (0.0, 2.0), "hat": 1.0, "clap": (), "bass": "half", "arp": 0.5},
|
|
1738
|
-
"drive": {"kick": (0.0, 2.0), "hat": 0.5, "clap": (1.0, 3.0),
|
|
1739
|
-
"bass": "off8", "arp": 0.5},
|
|
1740
|
-
"climax": {"kick": (0.0, 1.0, 2.0, 3.0), "hat": 0.25, "clap": (1.0, 3.0),
|
|
1741
|
-
"bass": "off8", "arp": 0.25},
|
|
1742
|
-
},
|
|
1743
|
-
"space": {"delay_beats": 1.0, "delay_db": -17.0, "taps": 2},
|
|
1744
|
-
"phrase_bars": 8,
|
|
1745
|
-
"duck": {"bass": 0.34, "pad": 0.24},
|
|
1746
|
-
"level_span_db": 7.0,
|
|
1747
|
-
"swing": 0.0,
|
|
1748
|
-
},
|
|
1749
|
-
# 手绘纸感:木吉他拨弦、马林巴、轻微摇摆;大调,温暖
|
|
1750
|
-
"paper": {
|
|
1751
|
-
"label": "手绘纸感",
|
|
1752
|
-
"bpm_hint": (84, 88, 92),
|
|
1753
|
-
"keys": ("G", "D", "C"),
|
|
1754
|
-
"progressions": {
|
|
1755
|
-
"calm": ("I", "IV", "vi", "V"),
|
|
1756
|
-
"body": ("I", "IV", "V", "vi"),
|
|
1757
|
-
"drive": ("I", "V", "IV", "I"),
|
|
1758
|
-
},
|
|
1759
|
-
"motif": {
|
|
1760
|
-
"q": ((0.0, "I", 1.0), (1.0, "III", 1.0), (2.0, "V", 1.0), (3.0, "VI", 1.5),
|
|
1761
|
-
(4.5, "V", 0.75), (5.25, "III", 0.75), (6.0, "I", 2.0)),
|
|
1762
|
-
"a": ((0.0, "III", 1.0), (1.0, "V", 1.0), (2.0, "VI", 1.0), (3.0, "V", 1.5),
|
|
1763
|
-
(4.5, "III", 0.75), (5.25, "V", 0.75), (6.0, "I", 2.0)),
|
|
1764
|
-
"end": ((0.0, "V", 1.0), (1.5, "VI", 1.0), (2.5, "V", 1.5), (4.0, "I", 3.5)),
|
|
1765
|
-
},
|
|
1766
|
-
"layers": {"pad": 0.0, "bass": 0.28, "kick": 0.50, "hat": 0.62,
|
|
1767
|
-
"arp": 0.45, "melody": 0.30, "shine": 0.88},
|
|
1768
|
-
"voices": {"pad": "strings", "bass": "cello", "arp": "pluck", "lead": "marimba"},
|
|
1769
|
-
"lead_cycle": ("marimba", "celesta"),
|
|
1770
|
-
"drums": {
|
|
1771
|
-
"calm": {"kick": (), "hat": None, "clap": (), "bass": "whole", "arp": None},
|
|
1772
|
-
"body": {"kick": (0.0, 2.0), "hat": 0.5, "clap": (), "bass": "half", "arp": 0.5},
|
|
1773
|
-
"drive": {"kick": (0.0, 2.0), "hat": 0.25, "clap": (1.0, 3.0),
|
|
1774
|
-
"bass": "off8", "arp": 0.25},
|
|
1775
|
-
"climax": {"kick": (0.0, 1.0, 2.0, 3.0), "hat": 0.25, "clap": (1.0, 3.0),
|
|
1776
|
-
"bass": "off8", "arp": 0.25},
|
|
1777
|
-
},
|
|
1778
|
-
"space": {"delay_beats": 0.5, "delay_db": -18.0, "taps": 2},
|
|
1779
|
-
"phrase_bars": 4,
|
|
1780
|
-
"duck": {"bass": 0.28, "pad": 0.22},
|
|
1781
|
-
"level_span_db": 7.5,
|
|
1782
|
-
"swing": 0.10,
|
|
1783
|
-
},
|
|
1784
|
-
# 活力撞色:大调 4/4、短切和弦、钟琴主奏;高能量、明亮
|
|
1785
|
-
"vivid": {
|
|
1786
|
-
"label": "活力撞色",
|
|
1787
|
-
"bpm_hint": (120, 126, 132),
|
|
1788
|
-
"keys": ("D", "G", "A"),
|
|
1789
|
-
"progressions": {
|
|
1790
|
-
"calm": ("I", "vi", "IV", "V"),
|
|
1791
|
-
"body": ("I", "V", "vi", "IV"),
|
|
1792
|
-
"drive": ("I", "IV", "V", "I"),
|
|
1793
|
-
},
|
|
1794
|
-
"motif": {
|
|
1795
|
-
"q": ((0.0, "V", 0.5), (0.5, "V", 0.25), (1.0, "VI", 0.5), (1.5, "V", 0.5),
|
|
1796
|
-
(2.0, "III", 0.5), (2.5, "I", 1.5), (4.0, "V", 0.5), (4.5, "VI", 0.5),
|
|
1797
|
-
(5.0, "V", 1.0), (6.0, "I", 2.0)),
|
|
1798
|
-
"a": ((0.0, "III", 0.5), (0.5, "III", 0.25), (1.0, "V", 0.5), (1.5, "III", 0.5),
|
|
1799
|
-
(2.0, "V", 0.5), (2.5, "VI", 1.5), (4.0, "V", 0.5), (5.0, "III", 1.0),
|
|
1800
|
-
(6.0, "I", 2.0)),
|
|
1801
|
-
"end": ((0.0, "V", 0.5), (1.0, "VI", 0.75), (2.0, "V", 1.5), (4.0, "I", 3.0)),
|
|
1802
|
-
},
|
|
1803
|
-
"layers": {"pad": 0.0, "bass": 0.20, "kick": 0.30, "hat": 0.40,
|
|
1804
|
-
"arp": 0.44, "melody": 0.48, "shine": 0.78},
|
|
1805
|
-
"voices": {"pad": "pad_saw", "bass": "pulse", "arp": "stab", "lead": "glock"},
|
|
1806
|
-
"lead_cycle": ("glock", "bell"),
|
|
1807
|
-
"drums": {
|
|
1808
|
-
"calm": {"kick": (), "hat": None, "clap": (), "bass": "whole", "arp": None},
|
|
1809
|
-
"body": {"kick": (0.0, 2.0), "hat": 0.5, "clap": (1.0, 3.0),
|
|
1810
|
-
"bass": "off8", "arp": 0.5},
|
|
1811
|
-
"drive": {"kick": (0.0, 1.0, 2.0, 3.0), "hat": 0.25, "clap": (1.0, 3.0),
|
|
1812
|
-
"bass": "16", "arp": 0.25},
|
|
1813
|
-
"climax": {"kick": (0.0, 1.0, 2.0, 3.0), "hat": 0.25, "clap": (1.0, 3.0),
|
|
1814
|
-
"bass": "16", "arp": 0.25},
|
|
1815
|
-
},
|
|
1816
|
-
"space": {"delay_beats": 0.5, "delay_db": -16.0, "taps": 3},
|
|
1817
|
-
"phrase_bars": 4,
|
|
1818
|
-
"duck": {"bass": 0.50, "pad": 0.32},
|
|
1819
|
-
"level_span_db": 9.0,
|
|
1820
|
-
"swing": 0.0,
|
|
1821
|
-
},
|
|
1822
|
-
# 极简黑白:只有必要的声音,长音、留白、钢片琴;慢速
|
|
1823
|
-
"mono": {
|
|
1824
|
-
"label": "极简黑白",
|
|
1825
|
-
"bpm_hint": (60, 64, 68),
|
|
1826
|
-
"keys": ("C", "F", "Am"),
|
|
1827
|
-
"progressions": {
|
|
1828
|
-
"calm": ("I", "IV", "I", "V"),
|
|
1829
|
-
"body": ("I", "IV", "V", "IV"),
|
|
1830
|
-
"drive": ("I", "IV", "V", "I"),
|
|
1831
|
-
},
|
|
1832
|
-
"motif": {
|
|
1833
|
-
"q": ((0.0, "I", 1.5), (1.5, "III", 1.5), (3.0, "V", 2.0),
|
|
1834
|
-
(6.0, "III", 1.0), (7.0, "V", 3.0)),
|
|
1835
|
-
"a": ((0.0, "V", 1.5), (1.5, "III", 1.5), (3.0, "I", 2.0),
|
|
1836
|
-
(6.0, "V", 1.0), (7.0, "I", 3.0)),
|
|
1837
|
-
"end": ((0.0, "V", 2.0), (2.0, "III", 1.5), (3.5, "I", 4.0)),
|
|
1838
|
-
},
|
|
1839
|
-
"layers": {"pad": 0.0, "bass": 0.34, "kick": 0.55, "hat": 0.70,
|
|
1840
|
-
"arp": 0.60, "melody": 0.25, "shine": 0.92},
|
|
1841
|
-
"voices": {"pad": "strings", "bass": "cello", "arp": "epiano", "lead": "celesta"},
|
|
1842
|
-
"lead_cycle": ("celesta", "bell"),
|
|
1843
|
-
"drums": {
|
|
1844
|
-
"calm": {"kick": (), "hat": None, "clap": (), "bass": "whole", "arp": None},
|
|
1845
|
-
"body": {"kick": (0.0,), "hat": None, "clap": (), "bass": "whole", "arp": None},
|
|
1846
|
-
"drive": {"kick": (0.0, 2.0), "hat": 1.0, "clap": (), "bass": "half", "arp": 0.5},
|
|
1847
|
-
"climax": {"kick": (0.0, 2.0), "hat": 0.5, "clap": (),
|
|
1848
|
-
"bass": "half", "arp": 0.5},
|
|
1849
|
-
},
|
|
1850
|
-
"space": {"delay_beats": 2.0, "delay_db": -20.0, "taps": 2},
|
|
1851
|
-
"phrase_bars": 8,
|
|
1852
|
-
"duck": {"bass": 0.30, "pad": 0.20},
|
|
1853
|
-
"level_span_db": 6.0,
|
|
1854
|
-
"swing": 0.0,
|
|
1855
|
-
},
|
|
1856
|
-
# 电影青橙:小调、弦乐铺底、大空间;安静到爆发跨度最大
|
|
1857
|
-
"cinema": {
|
|
1858
|
-
"label": "电影青橙",
|
|
1859
|
-
"bpm_hint": (76, 80, 84),
|
|
1860
|
-
"keys": ("Dm", "Am"),
|
|
1861
|
-
"progressions": {
|
|
1862
|
-
"calm": ("i", "VI", "III", "VII"),
|
|
1863
|
-
"body": ("i", "VII", "VI", "V"),
|
|
1864
|
-
"drive": ("i", "VI", "III", "VII"),
|
|
1865
|
-
},
|
|
1866
|
-
"motif": {
|
|
1867
|
-
"q": ((0.0, "i", 1.5), (1.5, "III", 1.0), (2.5, "iv", 2.0),
|
|
1868
|
-
(5.0, "III", 1.0), (6.0, "i", 3.0)),
|
|
1869
|
-
"a": ((0.0, "III", 1.5), (1.5, "VI", 1.0), (2.5, "VII", 2.0),
|
|
1870
|
-
(5.0, "v", 1.0), (6.0, "i", 3.0)),
|
|
1871
|
-
"end": ((0.0, "VI", 1.5), (1.5, "VII", 1.0), (2.5, "v", 1.5), (4.0, "i", 4.0)),
|
|
1872
|
-
},
|
|
1873
|
-
"layers": {"pad": 0.0, "bass": 0.26, "kick": 0.52, "hat": 0.68,
|
|
1874
|
-
"arp": 0.62, "melody": 0.30, "shine": 0.88},
|
|
1875
|
-
"voices": {"pad": "strings", "bass": "cello", "arp": "epiano",
|
|
1876
|
-
"lead": "strings_bell"},
|
|
1877
|
-
"lead_cycle": ("strings_bell", "celesta"),
|
|
1878
|
-
"drums": {
|
|
1879
|
-
"calm": {"kick": (), "hat": None, "clap": (), "bass": "whole", "arp": None},
|
|
1880
|
-
"body": {"kick": (0.0,), "hat": None, "clap": (), "bass": "whole", "arp": None},
|
|
1881
|
-
"drive": {"kick": (0.0, 2.0), "hat": 0.5, "clap": (), "bass": "half", "arp": 0.5},
|
|
1882
|
-
"climax": {"kick": (0.0, 1.0, 2.0, 3.0), "hat": 0.25, "clap": (1.0, 3.0),
|
|
1883
|
-
"bass": "off8", "arp": 0.25},
|
|
1884
|
-
},
|
|
1885
|
-
"space": {"delay_beats": 1.5, "delay_db": -14.0, "taps": 3},
|
|
1886
|
-
"phrase_bars": 8,
|
|
1887
|
-
"duck": {"bass": 0.40, "pad": 0.26},
|
|
1888
|
-
"level_span_db": 10.0,
|
|
1889
|
-
"swing": 0.0,
|
|
1890
|
-
},
|
|
1891
|
-
# 暖灰薄荷:lo-fi 摇摆、电钢、808 贝斯;轻鼓、留白
|
|
1892
|
-
"lo-fi": {
|
|
1893
|
-
"label": "暖灰薄荷",
|
|
1894
|
-
"bpm_hint": (72, 78, 84),
|
|
1895
|
-
"keys": ("C", "F", "G"),
|
|
1896
|
-
"progressions": {
|
|
1897
|
-
"calm": ("Imaj7", "vi7", "ii7", "V7"),
|
|
1898
|
-
"body": ("Imaj7", "IVmaj7", "iii7", "vi7"),
|
|
1899
|
-
"drive": ("ii7", "V7", "Imaj7", "IVmaj7"),
|
|
1900
|
-
},
|
|
1901
|
-
"motif": {
|
|
1902
|
-
"q": ((0.0, "III", 1.0), (1.0, "V", 1.0), (2.0, "VI", 1.0), (3.0, "V", 1.0),
|
|
1903
|
-
(4.0, "I", 1.0), (5.0, "ii", 1.0), (6.0, "III", 1.0), (7.0, "I", 2.0)),
|
|
1904
|
-
"a": ((0.0, "V", 1.0), (1.0, "VI", 1.0), (2.0, "V", 1.0), (3.0, "III", 1.0),
|
|
1905
|
-
(4.0, "ii", 1.0), (5.0, "III", 1.0), (6.0, "V", 1.0), (7.0, "I", 2.0)),
|
|
1906
|
-
"end": ((0.0, "V", 1.0), (1.0, "VI", 1.0), (2.0, "V", 1.0), (3.0, "I", 4.0)),
|
|
1907
|
-
},
|
|
1908
|
-
"layers": {"pad": 0.0, "bass": 0.24, "kick": 0.40, "hat": 0.48,
|
|
1909
|
-
"arp": 0.44, "melody": 0.32, "shine": 0.90},
|
|
1910
|
-
"voices": {"pad": "pad_saw", "bass": "sub", "arp": "epiano", "lead": "marimba"},
|
|
1911
|
-
"lead_cycle": ("marimba", "celesta"),
|
|
1912
|
-
"drums": {
|
|
1913
|
-
"calm": {"kick": (), "hat": None, "clap": (), "bass": "whole", "arp": None},
|
|
1914
|
-
"body": {"kick": (0.0, 2.0), "hat": 0.5, "clap": (), "bass": "half", "arp": 0.5},
|
|
1915
|
-
"drive": {"kick": (0.0, 2.0), "hat": 0.5, "clap": (1.0, 3.0),
|
|
1916
|
-
"bass": "off8", "arp": 0.5},
|
|
1917
|
-
"climax": {"kick": (0.0, 2.0), "hat": 0.25, "clap": (1.0, 3.0),
|
|
1918
|
-
"bass": "off8", "arp": 0.25},
|
|
1919
|
-
},
|
|
1920
|
-
"space": {"delay_beats": 0.75, "delay_db": -16.0, "taps": 2},
|
|
1921
|
-
"phrase_bars": 4,
|
|
1922
|
-
"duck": {"bass": 0.38, "pad": 0.26},
|
|
1923
|
-
"level_span_db": 6.5,
|
|
1924
|
-
"swing": 0.12,
|
|
1925
|
-
},
|
|
1926
|
-
# 深黑鎏金:小调、铜管主奏、四四拍重击;动态跨度最大
|
|
1927
|
-
"epic": {
|
|
1928
|
-
"label": "深黑鎏金",
|
|
1929
|
-
"bpm_hint": (88, 92, 96),
|
|
1930
|
-
"keys": ("Cm", "Dm"),
|
|
1931
|
-
"progressions": {
|
|
1932
|
-
"calm": ("i", "VI", "iv", "V"),
|
|
1933
|
-
"body": ("i", "VI", "III", "VII"),
|
|
1934
|
-
"drive": ("i", "VII", "VI", "V"),
|
|
1935
|
-
},
|
|
1936
|
-
"motif": {
|
|
1937
|
-
"q": ((0.0, "i", 1.0), (1.0, "III", 1.0), (2.0, "v", 1.0), (3.0, "VI", 2.0),
|
|
1938
|
-
(5.0, "v", 1.0), (6.0, "i", 3.0)),
|
|
1939
|
-
"a": ((0.0, "v", 1.0), (1.0, "VI", 1.0), (2.0, "VII", 1.0), (3.0, "V", 2.0),
|
|
1940
|
-
(5.0, "VII", 1.0), (6.0, "i", 3.0)),
|
|
1941
|
-
"end": ((0.0, "VI", 1.5), (1.5, "VII", 1.0), (2.5, "v", 1.0), (3.5, "i", 4.0)),
|
|
1942
|
-
},
|
|
1943
|
-
"layers": {"pad": 0.0, "bass": 0.20, "kick": 0.32, "hat": 0.42,
|
|
1944
|
-
"arp": 0.50, "melody": 0.30, "shine": 0.80},
|
|
1945
|
-
"voices": {"pad": "strings", "bass": "cello", "arp": "stab", "lead": "brass"},
|
|
1946
|
-
"lead_cycle": ("brass", "strings_bell"),
|
|
1947
|
-
"drums": {
|
|
1948
|
-
"calm": {"kick": (), "hat": None, "clap": (), "bass": "whole", "arp": None},
|
|
1949
|
-
"body": {"kick": (0.0, 2.0), "hat": 0.5, "clap": (), "bass": "half", "arp": 0.5},
|
|
1950
|
-
"drive": {"kick": (0.0, 1.0, 2.0, 3.0), "hat": 0.25, "clap": (1.0, 3.0),
|
|
1951
|
-
"bass": "16", "arp": 0.25},
|
|
1952
|
-
"climax": {"kick": (0.0, 1.0, 2.0, 3.0), "hat": 0.25, "clap": (1.0, 3.0),
|
|
1953
|
-
"bass": "16", "arp": 0.25},
|
|
1954
|
-
},
|
|
1955
|
-
"space": {"delay_beats": 0.75, "delay_db": -13.0, "taps": 3},
|
|
1956
|
-
"phrase_bars": 4,
|
|
1957
|
-
"duck": {"bass": 0.46, "pad": 0.34},
|
|
1958
|
-
"level_span_db": 11.0,
|
|
1959
|
-
"swing": 0.0,
|
|
1960
|
-
},
|
|
1961
|
-
# legacy:给 render_styled 用的"v0.4 近似包"(真实兼容路径不经过这里;
|
|
1962
|
-
# render(style=None) / render(style="legacy") 直接走老的 Score.render())
|
|
1963
|
-
"legacy": {
|
|
1964
|
-
"label": "v0.4 兼容",
|
|
1965
|
-
"bpm_hint": (120,),
|
|
1966
|
-
"keys": ("Dm", "Am", "Cm", "Em"),
|
|
1967
|
-
"progressions": {
|
|
1968
|
-
"calm": ("i", "VI", "iv", "v"),
|
|
1969
|
-
"body": ("i", "VI", "III", "VII"),
|
|
1970
|
-
"drive": ("i", "VII", "VI", "V"),
|
|
1971
|
-
},
|
|
1972
|
-
"motif": {
|
|
1973
|
-
"q": ((0.0, "i", 0.9), (1.0, "v", 0.9), (2.0, "I", 0.9), (3.0, "III", 1.8),
|
|
1974
|
-
(5.0, "i", 0.9), (6.0, "VII", 0.9), (7.0, "v", 1.6)),
|
|
1975
|
-
"a": ((0.0, "v", 0.9), (1.0, "i", 0.9), (2.0, "III", 0.9), (3.0, "iv", 0.9),
|
|
1976
|
-
(4.0, "v", 2.4)),
|
|
1977
|
-
"end": ((0.0, "v", 0.9), (1.0, "i", 0.9), (2.0, "III", 0.9), (3.0, "iv", 1.6),
|
|
1978
|
-
(5.0, "i", 3.0)),
|
|
1979
|
-
},
|
|
1980
|
-
"layers": {"pad": 0.0, "bass": 0.22, "kick": 0.34, "hat": 0.44,
|
|
1981
|
-
"arp": 0.55, "melody": 0.66, "shine": 0.82},
|
|
1982
|
-
"voices": {"pad": "pad_saw", "bass": "pulse", "arp": "fm", "lead": "bell"},
|
|
1983
|
-
"lead_cycle": ("bell", "celesta"),
|
|
1984
|
-
"drums": {
|
|
1985
|
-
"calm": {"kick": (), "hat": None, "clap": (), "bass": "whole", "arp": None},
|
|
1986
|
-
"body": {"kick": (0.0, 2.0), "hat": 1.0, "clap": (), "bass": "half", "arp": 0.5},
|
|
1987
|
-
"drive": {"kick": (0.0, 1.0, 2.0, 3.0), "hat": 0.25, "clap": (1.0, 3.0),
|
|
1988
|
-
"bass": "16", "arp": 0.25},
|
|
1989
|
-
"climax": {"kick": (0.0, 1.0, 2.0, 3.0), "hat": 0.25, "clap": (1.0, 3.0),
|
|
1990
|
-
"bass": "16", "arp": 0.25},
|
|
1991
|
-
},
|
|
1992
|
-
"space": {"delay_beats": 0.56, "delay_db": -15.0, "taps": 3},
|
|
1993
|
-
"phrase_bars": 4,
|
|
1994
|
-
"duck": {"bass": 0.42, "pad": 0.30},
|
|
1995
|
-
"level_span_db": 8.5,
|
|
1996
|
-
"swing": 0.0,
|
|
1997
|
-
},
|
|
1998
|
-
}
|
|
1999
|
-
|
|
2000
|
-
MUSIC_STYLE_IDS = tuple(MUSIC_STYLES)
|
|
2001
|
-
LEGACY_MUSIC_STYLE = "legacy"
|
|
2002
|
-
|
|
2003
|
-
# 情绪修饰:不改和弦功能,只改亮度 / 层数 / 鼓密度 / 主奏电平
|
|
2004
|
-
MOOD_IDS = ("steady", "calm", "reflective", "hopeful", "uplifting", "driving", "tense")
|
|
2005
|
-
_MOOD = {
|
|
2006
|
-
"steady": {"bright": 0.00, "layer": 0.00, "drum": 0.00, "lead_db": 0.0},
|
|
2007
|
-
"calm": {"bright": -0.18, "layer": 0.06, "drum": -0.06, "lead_db": -1.5},
|
|
2008
|
-
"reflective": {"bright": -0.12, "layer": 0.04, "drum": -0.04, "lead_db": -1.0},
|
|
2009
|
-
"hopeful": {"bright": 0.04, "layer": -0.02, "drum": 0.00, "lead_db": 0.5},
|
|
2010
|
-
"uplifting": {"bright": 0.14, "layer": -0.05, "drum": 0.05, "lead_db": 1.0},
|
|
2011
|
-
"driving": {"bright": 0.22, "layer": -0.07, "drum": 0.07, "lead_db": 1.2},
|
|
2012
|
-
"tense": {"bright": 0.02, "layer": 0.00, "drum": 0.03, "lead_db": 0.5},
|
|
2013
|
-
}
|
|
2014
|
-
|
|
2015
|
-
# 音色槽 -> 可选音色(实现见 _Bank 的 v0.5 方法;"fm" 复用老 FM 琶音)
|
|
2016
|
-
_STYLE_VOICES = {
|
|
2017
|
-
"pad": {"pad_saw": "pad", "strings": "strings"},
|
|
2018
|
-
"bass": {"pulse": "pulse", "cello": "cello", "sub": "sub"},
|
|
2019
|
-
"arp": {"pluck": "pluck", "epiano": "epiano", "stab": "stab", "fm": "fm"},
|
|
2020
|
-
"lead": {"bell": "bell", "celesta": "celesta", "marimba": "marimba",
|
|
2021
|
-
"glock": "glock", "strings_bell": "strings_bell", "brass": "brass",
|
|
2022
|
-
"epiano": "epiano"},
|
|
2023
|
-
}
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
def _normalize_style_id(style):
|
|
2027
|
-
"""None / "" / "legacy" -> None(走 v0.4 原路径);其余必须是 MUSIC_STYLES 的键。"""
|
|
2028
|
-
if style is None:
|
|
2029
|
-
return None
|
|
2030
|
-
sid = str(style).strip()
|
|
2031
|
-
if sid in ("", LEGACY_MUSIC_STYLE):
|
|
2032
|
-
return None
|
|
2033
|
-
_require(sid in MUSIC_STYLES, f"未知配乐风格 {style!r};可选:{'/'.join(MUSIC_STYLE_IDS)}")
|
|
2034
|
-
return sid
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
def _styled_tier(energy: float) -> str:
|
|
2038
|
-
"""风格化四档:calm / body / drive / climax(比 legacy 多一档,给 epic / vivid 用)。"""
|
|
2039
|
-
if energy < 0.40:
|
|
2040
|
-
return "calm"
|
|
2041
|
-
if energy < 0.70:
|
|
2042
|
-
return "body"
|
|
2043
|
-
if energy < 0.88:
|
|
2044
|
-
return "drive"
|
|
2045
|
-
return "climax"
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
def _styled_layers(pack: dict, energy: float, mood: str) -> list:
|
|
2049
|
-
"""按"层阈值 + mood 偏移"算本段出现的层;pad 恒在(配乐不能没有和声底)。"""
|
|
2050
|
-
m = _MOOD.get(mood, _MOOD["steady"])
|
|
2051
|
-
out = []
|
|
2052
|
-
for name in LAYER_ORDER:
|
|
2053
|
-
thr = float(pack["layers"][name])
|
|
2054
|
-
if name == "pad":
|
|
2055
|
-
out.append(name)
|
|
2056
|
-
continue
|
|
2057
|
-
if name == "bass":
|
|
2058
|
-
adj = 0.5 * m["layer"]
|
|
2059
|
-
elif name in ("kick", "hat"):
|
|
2060
|
-
adj = m["layer"] * 1.5 - m["drum"]
|
|
2061
|
-
else:
|
|
2062
|
-
adj = m["layer"]
|
|
2063
|
-
if energy >= max(0.0, thr + adj):
|
|
2064
|
-
out.append(name)
|
|
2065
|
-
return out
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
# ---- v0.7 内容指纹变奏(references/audio-variation.md §3.3)----------------
|
|
2069
|
-
# v0.6 及以前指纹只进 seed(只改音色细节):同风格 / 同 bpm / 同调、只换内容指纹,
|
|
2070
|
-
# 两段配乐相关性 0.966 —— 因为和弦进行 / 动机 / 鼓型全写死。
|
|
2071
|
-
# v0.7 把指纹推到"结构层":和弦做每两小节一次的稳定替换、动机级数做 ±1 稳定偏移、
|
|
2072
|
-
# 鼓型加 ghost 落点。三处决策全部走 _hash_int(FNV-1a,跨进程稳定),所以
|
|
2073
|
-
# 同指纹逐采样一致、不同指纹稳定地不同;没有引入额外随机源(变奏全部由指纹决定)。
|
|
2074
|
-
_VARIATION_SALT = "v0.7"
|
|
2075
|
-
|
|
2076
|
-
# 和弦替换对:同根音、同三音,只在"三和弦 <-> 七和弦色彩"之间换(契约 §3.3 的替代级数集)
|
|
2077
|
-
_CHORD_SWAPS = {
|
|
2078
|
-
"minor": {"i": "i7", "i7": "i", "VI": "VI7", "VI7": "VI",
|
|
2079
|
-
"III": "III7", "III7": "III", "VII": "VII7", "VII7": "VII"},
|
|
2080
|
-
"major": {"I": "Imaj7", "Imaj7": "I", "IV": "IVmaj7", "IVmaj7": "IV"},
|
|
2081
|
-
}
|
|
2082
|
-
|
|
2083
|
-
# 动机级数邻接表:只走"调内级数"的上下各一级(只换级数,节奏 / 时值不动)。
|
|
2084
|
-
# 值是 ((下方级数, 八度偏移), (上方级数, 八度偏移));跨主音时八度偏移 ±1,
|
|
2085
|
-
# 否则 i 下行一级会变成高 10 个半音的 VII(旋律轮廓会翻掉)。
|
|
2086
|
-
_MOTIF_NEIGHBORS = {
|
|
2087
|
-
"minor": {
|
|
2088
|
-
"i": (("VII", -1), ("ii°", 0)), "ii°": (("i", 0), ("III", 0)),
|
|
2089
|
-
"III": (("ii°", 0), ("iv", 0)), "iv": (("III", 0), ("v", 0)),
|
|
2090
|
-
"v": (("iv", 0), ("VI", 0)), "V": (("iv", 0), ("VI", 0)),
|
|
2091
|
-
"VI": (("v", 0), ("VII", 0)), "VII": (("VI", 0), ("i", 1)),
|
|
2092
|
-
"I": (("VII", -1), ("ii°", 0)), "i7": (("VII", -1), ("ii°", 0)),
|
|
2093
|
-
},
|
|
2094
|
-
"major": {
|
|
2095
|
-
"I": (("vii", -1), ("ii", 0)), "vii": (("vi", 0), ("I", 1)),
|
|
2096
|
-
"ii": (("I", 0), ("iii", 0)), "iii": (("ii", 0), ("IV", 0)),
|
|
2097
|
-
"IV": (("iii", 0), ("V", 0)), "V": (("IV", 0), ("vi", 0)),
|
|
2098
|
-
"vi": (("V", 0), ("vii", 0)), "VI": (("V", 0), ("VII", 0)),
|
|
2099
|
-
"III": (("ii", 0), ("IV", 0)), "iv": (("iii", 0), ("V", 0)),
|
|
2100
|
-
"Imaj7": (("vii", -1), ("ii", 0)), "IVmaj7": (("iii", 0), ("V", 0)),
|
|
2101
|
-
},
|
|
2102
|
-
}
|
|
2103
|
-
|
|
2104
|
-
# 进行档位(按"安静 -> 推进"排);climax 复用 drive(v0.5 语义:高潮靠鼓与层数)
|
|
2105
|
-
_PROG_TIERS = ("calm", "body", "drive")
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
def _styled_mode(pack: dict) -> str:
|
|
2109
|
-
"""风格包是小调还是大调:看 progressions 的主和弦写法(i/i7 = 小调,I/Imaj7 = 大调)。
|
|
2110
|
-
|
|
2111
|
-
调式只决定"替换对 / 级数邻接表"用哪一套;真正的调性仍由 key 决定,两者不改动。
|
|
2112
|
-
"""
|
|
2113
|
-
degs = {str(d) for seq in pack["progressions"].values() for d in seq}
|
|
2114
|
-
return "minor" if (degs & {"i", "i7"}) else "major"
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
def _styled_has_fp(fingerprint) -> str:
|
|
2118
|
-
"""指纹归一化:None / "" -> ""(= 关掉全部结构变奏,与 v0.5 逐字节一致)。"""
|
|
2119
|
-
if fingerprint is None:
|
|
2120
|
-
return ""
|
|
2121
|
-
s = str(fingerprint)
|
|
2122
|
-
return s if s != "" else ""
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
def _styled_progression(pack: dict, tier: str, mood: str, fingerprint=None) -> tuple:
|
|
2126
|
-
"""取该 tier 的级数进行;climax 复用 drive(高潮靠鼓与层数,不靠换和弦)。
|
|
2127
|
-
|
|
2128
|
-
v0.7:给了指纹时,按指纹从该风格的 progressions 里另选一档(契约 §3.3)——
|
|
2129
|
-
同一风格的两部片可以拿到不同的和弦功能顺序,但调式 / 音色 / 鼓型 / 段落结构不变。
|
|
2130
|
-
偏移量对整部片只有一个(哈希里不带段落号),片内各段的进行关系因此仍然连贯;
|
|
2131
|
-
无指纹 -> 与 v0.5 完全同一结果(只按能量档取)。
|
|
2132
|
-
"""
|
|
2133
|
-
progs = pack["progressions"]
|
|
2134
|
-
key = tier if tier in progs else ("drive" if tier == "climax" else "body")
|
|
2135
|
-
fp = _styled_has_fp(fingerprint)
|
|
2136
|
-
tiers = [t for t in _PROG_TIERS if t in progs]
|
|
2137
|
-
if fp and tiers and tier != "climax": # climax 仍复用 drive(v0.5 语义:高潮靠鼓与层数)
|
|
2138
|
-
base = tiers.index(key) if key in tiers else min(1, len(tiers) - 1)
|
|
2139
|
-
off = _hash_int(_VARIATION_SALT, "tier", fp) % len(tiers)
|
|
2140
|
-
key = tiers[(base + off) % len(tiers)]
|
|
2141
|
-
return tuple(progs[key])
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
def _styled_chord_rules(deg: str, j: int, nb: int, ph: int, tier: str,
|
|
2145
|
-
mood: str, is_last_sec: bool) -> str:
|
|
2146
|
-
"""单小节的硬规则(与 v0.5 逐字同序):乐句末属和弦 -> mood 色彩 -> 片尾主和弦。
|
|
2147
|
-
|
|
2148
|
-
单独拆出来是为了让"变奏前 / 变奏后"各跑一遍:替换是否真的落地、有没有被
|
|
2149
|
-
硬规则吃掉(乐句末收 V、片尾落 I)都能算清楚,report 里的计数才是真数。
|
|
2150
|
-
"""
|
|
2151
|
-
# 每两乐句收一次属和弦(制造张力,与 legacy 的写法同源)
|
|
2152
|
-
if j % ph == ph - 1 and (j // ph) % 2 == 1:
|
|
2153
|
-
deg = "V"
|
|
2154
|
-
# mood 色彩:hopeful 加 maj7 / sus2;tense 用 sus4 悬留
|
|
2155
|
-
if mood == "hopeful":
|
|
2156
|
-
if deg == "I":
|
|
2157
|
-
deg = "Imaj7"
|
|
2158
|
-
elif deg == "IV":
|
|
2159
|
-
deg = "IVmaj7"
|
|
2160
|
-
elif deg == "i":
|
|
2161
|
-
deg = "i7"
|
|
2162
|
-
elif j == 0 and tier == "calm":
|
|
2163
|
-
deg = "sus2"
|
|
2164
|
-
elif mood == "tense" and j % ph == ph - 2 and deg in ("i", "I"):
|
|
2165
|
-
deg = "sus4"
|
|
2166
|
-
# 片尾落主和弦(与 legacy 同口径:I = 皮卡迪三度 / 大调主)
|
|
2167
|
-
if is_last_sec and nb >= 2 and j >= nb - 2:
|
|
2168
|
-
deg = "I"
|
|
2169
|
-
return deg
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
def _styled_chords(sec: dict, si: int, secs: list, prog: tuple, pack: dict, mood: str,
|
|
2173
|
-
fingerprint=None, mode: str = "minor") -> tuple:
|
|
2174
|
-
"""一小节一个和弦:级数循环 + 乐句末收属和弦 + mood 色彩 + 片尾落 I。
|
|
2175
|
-
|
|
2176
|
-
v0.7:给了指纹时,**每两小节做一次稳定替换**(契约 §3.3):按指纹决定替换
|
|
2177
|
-
块内第 1 小节还是第 2 小节,被选中的小节在 _CHORD_SWAPS 里换成同根音的七和弦
|
|
2178
|
-
色彩(该小节级数不在替代集里就跳过)。替换先于硬规则执行,所以乐句末属和弦与
|
|
2179
|
-
片尾主和弦永远不被改坏(契约的"只在不冲突时替换")。
|
|
2180
|
-
返回 (和弦级数列表, 实际替换次数);无指纹时替换次数恒 0、列表与 v0.5 逐项一致。
|
|
2181
|
-
"""
|
|
2182
|
-
nb = len(sec["bars"])
|
|
2183
|
-
ph = int(pack["phrase_bars"])
|
|
2184
|
-
tier = sec["tier"]
|
|
2185
|
-
is_last_sec = (si == len(secs) - 1)
|
|
2186
|
-
fp = _styled_has_fp(fingerprint)
|
|
2187
|
-
smap = _CHORD_SWAPS.get(mode, {})
|
|
2188
|
-
out = []
|
|
2189
|
-
swaps = 0
|
|
2190
|
-
for j in range(nb):
|
|
2191
|
-
base = prog[j % len(prog)]
|
|
2192
|
-
deg = _styled_chord_rules(base, j, nb, ph, tier, mood, is_last_sec)
|
|
2193
|
-
if fp and smap:
|
|
2194
|
-
# 每两小节一次稳定替换:按指纹决定替换块内第 1 还是第 2 小节
|
|
2195
|
-
# (该小节级数不在替代集里就跳过,不硬造不存在的替代级数)
|
|
2196
|
-
pick = _hash_int(_VARIATION_SALT, "chord", fp, si, j // 2) % 2
|
|
2197
|
-
if pick == (j % 2) and base in smap:
|
|
2198
|
-
alt = _styled_chord_rules(smap[base], j, nb, ph, tier, mood, is_last_sec)
|
|
2199
|
-
if alt != deg: # 被硬规则吃掉的替换不算一次替换
|
|
2200
|
-
deg = alt
|
|
2201
|
-
swaps += 1
|
|
2202
|
-
out.append(deg)
|
|
2203
|
-
return out, swaps
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
def _styled_kick_ghost(cfg: dict, fp: str, si: int, j: int,
|
|
2207
|
-
fill_bar: bool, breath: bool):
|
|
2208
|
-
"""按指纹给这一小节加一个 kick ghost 落点;返回落点(拍)或 None。
|
|
2209
|
-
|
|
2210
|
-
ghost 只落在 0.5 拍网格上、不与 cfg["kick"] 现有落点重合;fill / breath 小节
|
|
2211
|
-
只允许落在 [0, 3) 拍内(那几个位置要留给鼓加花与呼吸,契约 §3.3)。
|
|
2212
|
-
每小节最多一记,1/3 的小节不加 —— 否则"每小节都多一记"会变成另一种机械。
|
|
2213
|
-
"""
|
|
2214
|
-
used = [float(x) for x in cfg.get("kick", ())]
|
|
2215
|
-
cand = []
|
|
2216
|
-
for k in range(int(round(BEATS_PER_BAR / 0.5))):
|
|
2217
|
-
pos = 0.5 * k
|
|
2218
|
-
if any(abs(pos - u) < 1e-9 for u in used):
|
|
2219
|
-
continue
|
|
2220
|
-
if (fill_bar or breath) and pos >= 3.0 - 1e-9:
|
|
2221
|
-
continue
|
|
2222
|
-
cand.append(pos)
|
|
2223
|
-
if not cand:
|
|
2224
|
-
return None
|
|
2225
|
-
h = _hash_int(_VARIATION_SALT, "ghost", fp, si, j)
|
|
2226
|
-
if h % 3 == 0:
|
|
2227
|
-
return None
|
|
2228
|
-
return cand[(h >> 3) % len(cand)]
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
def _styled_clap_shift(fp: str, si: int, j: int, idx: int) -> float:
|
|
2232
|
-
"""拍手位置的指纹位移:±0.5 拍(契约 §3.3);1/3 不动,保留原律动的稳定感。"""
|
|
2233
|
-
h = _hash_int(_VARIATION_SALT, "clap", fp, si, j, idx)
|
|
2234
|
-
m = h % 3
|
|
2235
|
-
return 0.0 if m == 2 else (0.5 if m == 0 else -0.5)
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
def _styled_target_db(energy: float, span: float) -> float:
|
|
2239
|
-
"""能量 -> 相对电平(dB)。span = 最安静段到最响段的电平跨度;
|
|
2240
|
-
|
|
2241
|
-
span=8.5 时与 legacy 的 energy_target_db() 完全同式:-8.5 * (1 - energy)。
|
|
2242
|
-
"""
|
|
2243
|
-
return -float(span) * (1.0 - _clip01(energy))
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
def _styled_swing_shift(swing: float, step: float, k: int) -> float:
|
|
2247
|
-
"""摇摆:只延后"奇数细分"(不影响 RNG 调用次数,只改落点)。"""
|
|
2248
|
-
if swing <= 0.0 or step >= 1.0 or (k % 2) == 0:
|
|
2249
|
-
return 0.0
|
|
2250
|
-
return float(swing) * min(1.0, step / 0.5)
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
def _degree_note(kval: dict, degree: str, octave: int = 4) -> str:
|
|
2254
|
-
"""级数 -> 音名(相对主音,跨八度自动进位)。"""
|
|
2255
|
-
row = _DEGREE_BY_NAME[degree]
|
|
2256
|
-
return _semi_note(kval["root_pc"], int(octave), int(row[1]), kval["names"])
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
def _check_voice(slot: str, voice: str) -> str:
|
|
2260
|
-
"""校验风格音色名并返回内部实现名(未知音色直接报错,不静默回落)。"""
|
|
2261
|
-
table = _STYLE_VOICES[slot]
|
|
2262
|
-
_require(voice in table, f"风格音色 {slot}={voice!r} 未知;可选:{'/'.join(table)}")
|
|
2263
|
-
return table[voice]
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
def _bass_texture(bank: "_Bank", voice: str, note: str, dur: float, bright: float) -> np.ndarray:
|
|
2267
|
-
if voice == "cello":
|
|
2268
|
-
return bank.cello(note, dur, bright)
|
|
2269
|
-
if voice == "sub":
|
|
2270
|
-
return bank.sub_bass(note, dur)
|
|
2271
|
-
return bank.pulse_bass(note, dur, bright)
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
def _arp_texture(bank: "_Bank", voice: str, note: str, bright: float, dur: float = 0.3) -> np.ndarray:
|
|
2275
|
-
if voice == "pluck":
|
|
2276
|
-
return bank.pluck_note(note, dur, bright)
|
|
2277
|
-
if voice == "epiano":
|
|
2278
|
-
return bank.epiano(note, max(0.35, dur))
|
|
2279
|
-
if voice == "stab":
|
|
2280
|
-
return bank.stab(note, dur, bright)
|
|
2281
|
-
return bank.arp(note, bright, dur)
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
def _lead_texture(bank: "_Bank", voice: str, note: str, dur: float, bright: float) -> np.ndarray:
|
|
2285
|
-
if voice == "celesta":
|
|
2286
|
-
return bank.celesta(note, max(2.8, dur))
|
|
2287
|
-
if voice == "marimba":
|
|
2288
|
-
return bank.marimba(note, max(0.6, dur))
|
|
2289
|
-
if voice == "glock":
|
|
2290
|
-
return bank.glock(note, max(0.5, dur))
|
|
2291
|
-
if voice == "strings_bell":
|
|
2292
|
-
return bank.strings_bell(note, max(1.2, dur))
|
|
2293
|
-
if voice == "brass":
|
|
2294
|
-
return bank.brass(note, max(0.5, dur), bright)
|
|
2295
|
-
if voice == "epiano":
|
|
2296
|
-
return bank.epiano(note, max(0.8, dur))
|
|
2297
|
-
return bank.bell(note, max(2.4, dur))
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
def _styled_motif_shift(motif, kind: str, fingerprint, style: str, mode: str) -> tuple:
|
|
2301
|
-
"""按指纹给动机的每个事件派生 ±1 级数偏移;返回 (事件列表, 偏移次数)。
|
|
2302
|
-
|
|
2303
|
-
只改级数不改节奏:拍位与时值原样保留。偏移方向按 "指纹 + 风格 + 动机类型 + 事件序号"
|
|
2304
|
-
哈希派生,所以问句(q) / 答句(a) / 片尾(end) **各自独立**(契约 §3.3 的"独立派生"):
|
|
2305
|
-
同一部片里同种动机稳定,两部片不会同步变形(q 上移的片子 a 未必也上移)。
|
|
2306
|
-
返回的事件是 (拍, 级数, 时值, 八度偏移):跨主音上行 / 下行时用八度偏移修正,
|
|
2307
|
-
否则"小调 i 下行一级"会变成高 10 个半音的 VII,旋律轮廓会翻掉。
|
|
2308
|
-
"""
|
|
2309
|
-
fp = _styled_has_fp(fingerprint)
|
|
2310
|
-
table = _MOTIF_NEIGHBORS.get(mode, {})
|
|
2311
|
-
if not fp or not table:
|
|
2312
|
-
return [(float(ev[0]), str(ev[1]), float(ev[2]), 0) for ev in motif], 0
|
|
2313
|
-
# 整句的偏移方向只由 "指纹 + 风格 + 动机类型" 决定(同种动机稳定),
|
|
2314
|
-
# 每个事件在±1 里取哪边由事件序号决定 —— q 与 a 的方向各自独立派生。
|
|
2315
|
-
up = _hash_int(_VARIATION_SALT, "motif-dir", fp, style, kind) % 2 == 1
|
|
2316
|
-
out = []
|
|
2317
|
-
shifted = 0
|
|
2318
|
-
for idx, ev in enumerate(motif):
|
|
2319
|
-
pos, deg, dur = float(ev[0]), str(ev[1]), float(ev[2])
|
|
2320
|
-
pair = table.get(deg)
|
|
2321
|
-
h = _hash_int(_VARIATION_SALT, "motif", fp, style, kind, idx)
|
|
2322
|
-
if pair is None: # 邻接表没收录的级数不动,绝不瞎编级数
|
|
2323
|
-
out.append((pos, deg, dur, 0))
|
|
2324
|
-
continue
|
|
2325
|
-
name, oct_delta = pair[1] if (up != bool(h % 2)) else pair[0]
|
|
2326
|
-
out.append((pos, name, dur, int(oct_delta)))
|
|
2327
|
-
shifted += 1
|
|
2328
|
-
return out, shifted
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
def _emit_styled_motif(bus: "_Bus", bank: "_Bank", motif, kval: dict, voice: str,
|
|
2332
|
-
t0: float, beat: float, db: float, bright: float,
|
|
2333
|
-
fingerprint=None, style: str = "", kind: str = "",
|
|
2334
|
-
mode: str = "minor") -> int:
|
|
2335
|
-
"""把风格动机铺到主奏总线:级数 -> 音名,乐器由 lead_cycle 指定。
|
|
2336
|
-
|
|
2337
|
-
v0.7:先按指纹做级数 ±1 偏移(_styled_motif_shift)再铺;返回被偏移的事件数,
|
|
2338
|
-
供 report["variation"]["motif_shifts"] 审计。无指纹时偏移数恒 0、逐音与 v0.5 相同。
|
|
2339
|
-
"""
|
|
2340
|
-
events, shifted = _styled_motif_shift(motif, kind, fingerprint, style, mode)
|
|
2341
|
-
for idx, ev in enumerate(events):
|
|
2342
|
-
pos_beats = float(ev[0])
|
|
2343
|
-
degree, dur_beats, oct_delta = str(ev[1]), float(ev[2]), int(ev[3])
|
|
2344
|
-
note = _degree_note(kval, degree, 4 + oct_delta)
|
|
2345
|
-
tex = _lead_texture(bank, voice, note, dur_beats * beat, bright)
|
|
2346
|
-
pan = 0.18 if idx % 2 == 0 else -0.18
|
|
2347
|
-
bus.add(tex, t0 + pos_beats * beat, db, pan)
|
|
2348
|
-
return shifted
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
def _styled_gate_env(n: int, secs: list, present: list, bar: float, sr: int = SR) -> np.ndarray:
|
|
2352
|
-
"""层进出门控:某层在相邻段缺席时,用 1 小节线性坡道渐入 / 渐出。"""
|
|
2353
|
-
env = np.ones(int(n), np.float32)
|
|
2354
|
-
L = max(2, _ns(bar, sr))
|
|
2355
|
-
nsec = len(secs)
|
|
2356
|
-
for si, sec in enumerate(secs):
|
|
2357
|
-
s = max(0, min(int(n), int(round(float(sec["start"]) * sr))))
|
|
2358
|
-
e = max(0, min(int(n), int(round(float(sec["end"]) * sr))))
|
|
2359
|
-
if e - s < 2:
|
|
2360
|
-
continue
|
|
2361
|
-
if not present[si]:
|
|
2362
|
-
env[s:e] = 0.0
|
|
2363
|
-
continue
|
|
2364
|
-
prev = present[si - 1] if si > 0 else present[si]
|
|
2365
|
-
nxt = present[si + 1] if si + 1 < nsec else present[si]
|
|
2366
|
-
if not prev:
|
|
2367
|
-
seg = env[s:min(e, s + L)]
|
|
2368
|
-
seg *= _f32(np.linspace(0.0, 1.0, len(seg)) ** 1.5)
|
|
2369
|
-
if not nxt:
|
|
2370
|
-
seg = env[max(s, e - L):e]
|
|
2371
|
-
seg *= _f32(np.linspace(1.0, 0.0, len(seg)) ** 1.5)
|
|
2372
|
-
return env
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
def _styled_seed(seed: int, fingerprint, style: str, mood: str) -> int:
|
|
2376
|
-
"""内容指纹 -> 音色细节 seed(只影响 v0.5 风格路径;legacy 不参与)。"""
|
|
2377
|
-
base = int(seed)
|
|
2378
|
-
if fingerprint is None or str(fingerprint) == "":
|
|
2379
|
-
return base
|
|
2380
|
-
h = _hash_int("v0.5", style, mood, str(fingerprint))
|
|
2381
|
-
return base * 1000 + (h % 1000)
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
def _styled_params(style: str, bpm, key, mood, seed, music_plan) -> dict:
|
|
2385
|
-
"""合并显式参数与 stylekit 的 music dict:plan 优先,显式参数只在 plan 缺项时兜底。
|
|
2386
|
-
|
|
2387
|
-
music_plan 可以是 resolve_script_style()["music"](推荐),也可以是整份 plan(含 music 子字典)。
|
|
2388
|
-
缺省回退顺序:plan -> 显式参数 -> 风格包自带值(bpm_hint 中位 / 调池首个 / steady / 7)。
|
|
2389
|
-
"""
|
|
2390
|
-
pack = MUSIC_STYLES[style]
|
|
2391
|
-
hint = pack["bpm_hint"]
|
|
2392
|
-
eff = {
|
|
2393
|
-
"bpm": None if bpm is None else float(bpm),
|
|
2394
|
-
"key": None if key is None else str(key),
|
|
2395
|
-
"mood": "steady" if mood is None else str(mood),
|
|
2396
|
-
"seed": 7 if seed is None else int(seed),
|
|
2397
|
-
}
|
|
2398
|
-
plan = music_plan
|
|
2399
|
-
if isinstance(plan, dict):
|
|
2400
|
-
if isinstance(plan.get("music"), dict):
|
|
2401
|
-
plan = plan["music"]
|
|
2402
|
-
if plan.get("bpm") not in (None, ""):
|
|
2403
|
-
eff["bpm"] = float(plan["bpm"])
|
|
2404
|
-
if plan.get("key") not in (None, ""):
|
|
2405
|
-
eff["key"] = str(plan["key"])
|
|
2406
|
-
if plan.get("mood") not in (None, ""):
|
|
2407
|
-
eff["mood"] = str(plan["mood"]).strip().lower() or "steady"
|
|
2408
|
-
# seed_effective 是 stylekit 已经掺过内容指纹的最终种子;没有就退回 seed
|
|
2409
|
-
for fld in ("seed_effective", "seed"):
|
|
2410
|
-
if plan.get(fld) is not None:
|
|
2411
|
-
try:
|
|
2412
|
-
eff["seed"] = abs(int(plan[fld]))
|
|
2413
|
-
break
|
|
2414
|
-
except (TypeError, ValueError):
|
|
2415
|
-
continue
|
|
2416
|
-
if eff["bpm"] is None:
|
|
2417
|
-
eff["bpm"] = float(hint[len(hint) // 2])
|
|
2418
|
-
if eff["key"] is None:
|
|
2419
|
-
eff["key"] = str(pack["keys"][0])
|
|
2420
|
-
eff["mood"] = str(eff["mood"] or "steady").strip().lower() or "steady"
|
|
2421
|
-
return eff
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
def _apply_pace(blocks: list, pace) -> list:
|
|
2425
|
-
"""按 pacing 的 min_dur + hold 把过碎的段落合并到下限(不改片长、不挪时间轴)。
|
|
2426
|
-
|
|
2427
|
-
stylekit 的 min_dur 是"一条字幕至少停留多久",hold 是"句末额外留白";
|
|
2428
|
-
这里只用两者之和当"段落最短长度":太碎的锚点(例如 1 小节一个)会被并进前一段,
|
|
2429
|
-
名字保留前一段,energy 取较高者(合并后的段要能代表最响的那一刻)。
|
|
2430
|
-
"""
|
|
2431
|
-
if not pace or not isinstance(pace, dict) or not blocks:
|
|
2432
|
-
return blocks
|
|
2433
|
-
try:
|
|
2434
|
-
lo = float(pace.get("min_dur", 0.0)) + float(pace.get("hold", 0.0))
|
|
2435
|
-
except (TypeError, ValueError):
|
|
2436
|
-
return blocks
|
|
2437
|
-
if lo <= 0.0:
|
|
2438
|
-
return blocks
|
|
2439
|
-
out = [dict(blocks[0])]
|
|
2440
|
-
for b in blocks[1:]:
|
|
2441
|
-
prev = out[-1]
|
|
2442
|
-
if float(prev["end"]) - float(prev["start"]) < lo - 1e-9:
|
|
2443
|
-
prev["end"] = float(b["end"])
|
|
2444
|
-
prev["energy"] = max(float(prev["energy"]), float(b["energy"]))
|
|
2445
|
-
else:
|
|
2446
|
-
out.append(dict(b))
|
|
2447
|
-
while len(out) > 1 and (float(out[-1]["end"]) - float(out[-1]["start"])) < lo - 1e-9:
|
|
2448
|
-
last = out.pop()
|
|
2449
|
-
out[-1]["end"] = float(last["end"])
|
|
2450
|
-
out[-1]["energy"] = max(float(out[-1]["energy"]), float(last["energy"]))
|
|
2451
|
-
return out
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
def render_styled(duration, *, style, bpm=None, key=None, mood=None, anchors=None,
|
|
2455
|
-
sections=None, seed=7, fingerprint=None, sucks=None,
|
|
2456
|
-
music_plan=None, pace=None) -> tuple:
|
|
2457
|
-
"""按风格渲一条配乐(v0.5):返回 (samples, report)。
|
|
2458
|
-
|
|
2459
|
-
与 render() 的区别只在"怎么写",对外保证相同:
|
|
2460
|
-
* 时长精确(ceil(duration*sr) 采样)、峰值 <= -1.5 dBFS、同 seed 逐样本一致;
|
|
2461
|
-
* 复用老 _Bank 的鼓组与 _Bus 总线,只新增音色方法,不重写任何老乐器;
|
|
2462
|
-
* 老路径(render(style=None) / "legacy")完全不受影响,逐样本与 v0.4 相同。
|
|
2463
|
-
|
|
2464
|
-
anchors / sections 语义与 render() 一致;mood 见 MOOD_IDS。
|
|
2465
|
-
fingerprint(v0.7):参与 seed 派生 + 结构层变奏(契约 §3.3)——和弦每两小节一次
|
|
2466
|
-
七和弦色彩替换、按指纹选进行档、动机级数 ±1 稳定偏移、鼓型 ghost 落点 / clap 位移。
|
|
2467
|
-
None / "" 时三处变奏全部关闭,逐样本与 v0.5 相同;报告里的 variation 是这三项的计数。
|
|
2468
|
-
|
|
2469
|
-
music_plan:stylekit.resolve_script_style(script) 的 "music" 子字典(整份 plan 也接受)。
|
|
2470
|
-
非 None 时 bpm / key / mood / seed 以它为准(优先级高于显式参数)——render.py 只需
|
|
2471
|
-
传一个 dict,不必和两处默认值打架;显式参数退化成"plan 里没写时的缺省"。
|
|
2472
|
-
pace:stylekit 的 pacing dict(至少含 min_dur / hold)。只按"min_dur + hold"把过碎的段落
|
|
2473
|
-
合并到下限,不改 anchors 时间轴、不改片长;没有 pacing 需求时传 None 即可。
|
|
2474
|
-
|
|
2475
|
-
性能:styled 比 legacy 慢约 1.5~2.5x(多声部弦乐 + 新音色是主要成本);
|
|
2476
|
-
本机实测 60 s 片 3~6 s、388 s 片约 20~40 s(legacy 60 s ≈ 2.4 s),属预期。
|
|
2477
|
-
"""
|
|
2478
|
-
t_start = time.perf_counter()
|
|
2479
|
-
_require(style in MUSIC_STYLES, f"未知配乐风格 {style!r};可选:{'/'.join(MUSIC_STYLE_IDS)}")
|
|
2480
|
-
eff = _styled_params(style, bpm, key, mood, seed, music_plan)
|
|
2481
|
-
bpm, key, mood, seed = eff["bpm"], eff["key"], eff["mood"], eff["seed"]
|
|
2482
|
-
_require(mood in MOOD_IDS, f"未知情绪 {mood!r};可选:{'/'.join(MOOD_IDS)}")
|
|
2483
|
-
_require(isinstance(bpm, (int, float)) and math.isfinite(float(bpm)) and float(bpm) > 0.0,
|
|
2484
|
-
f"bpm 必须是有限正数,收到 {bpm!r}")
|
|
2485
|
-
_require(key in KEYS_ALL, f"未知调性 {key!r};可选:{'/'.join(KEYS_ALL)}")
|
|
2486
|
-
pack = MUSIC_STYLES[style]
|
|
2487
|
-
# v0.7:调式决定和弦替换对 / 级数邻接表用哪一套;fp_s 为空 = 全部结构变奏关闭
|
|
2488
|
-
mode = _styled_mode(pack)
|
|
2489
|
-
fp_s = _styled_has_fp(fingerprint)
|
|
2490
|
-
dur = float(duration)
|
|
2491
|
-
sr = SR
|
|
2492
|
-
sc = Score(duration, bpm=float(bpm), key=key, seed=int(seed))
|
|
2493
|
-
# _merge_sections 在没有 anchors/sections 时已经返回"全片",这里不要再补一段:
|
|
2494
|
-
# 补了会在非小节对齐的片长上多出一个 0.x 秒的重复段(legacy 路径保留该历史行为)。
|
|
2495
|
-
for s in _apply_pace(_merge_sections(anchors, sections, dur), pace):
|
|
2496
|
-
sc.section(s["name"], s["start"], s["end"], s["energy"])
|
|
2497
|
-
for t, db, d in (sucks or ()):
|
|
2498
|
-
sc.suck(t, db, d)
|
|
2499
|
-
plan = sc._build_plan()
|
|
2500
|
-
secs = plan["sections"]
|
|
2501
|
-
bar, beat = plan["bar_s"], plan["beat_s"]
|
|
2502
|
-
n = max(int(plan["n"]), int(math.ceil(dur * sr - 1e-9)))
|
|
2503
|
-
kval = sc.k
|
|
2504
|
-
seed_eff = _styled_seed(sc.seed, fingerprint, style, mood)
|
|
2505
|
-
bank = _Bank(seed_eff, beat, bar, sr)
|
|
2506
|
-
|
|
2507
|
-
m = _MOOD[mood]
|
|
2508
|
-
mood_bright = float(m["bright"])
|
|
2509
|
-
span = float(pack["level_span_db"])
|
|
2510
|
-
ph = int(pack["phrase_bars"])
|
|
2511
|
-
swing = float(pack["swing"])
|
|
2512
|
-
space = pack["space"]
|
|
2513
|
-
|
|
2514
|
-
# ---- 1) 段落规划:tier / 层 / 和弦 / 电平目标 ----
|
|
2515
|
-
# v0.7:指纹变奏计数(chord_swaps / motif_shifts / drum_ghosts)进 report["variation"]。
|
|
2516
|
-
# 无指纹时三处变奏全关、计数恒 0,和弦仍走 v0.5 那条"只按能量档取进行"的分支。
|
|
2517
|
-
chord_swaps = 0
|
|
2518
|
-
motif_shifts = 0
|
|
2519
|
-
drum_ghosts = 0
|
|
2520
|
-
for si, sec in enumerate(secs):
|
|
2521
|
-
e = sec["energy"]
|
|
2522
|
-
tier = _styled_tier(e)
|
|
2523
|
-
sec["tier"] = tier
|
|
2524
|
-
sec["layers"] = _styled_layers(pack, e, mood)
|
|
2525
|
-
prog = _styled_progression(pack, tier, mood, fingerprint)
|
|
2526
|
-
sec["chords"], n_swap = _styled_chords(sec, si, secs, prog, pack, mood,
|
|
2527
|
-
fingerprint, mode)
|
|
2528
|
-
chord_swaps += n_swap
|
|
2529
|
-
sec["target_db"] = round(_styled_target_db(e, span), 2)
|
|
2530
|
-
sec["target_lufs"] = round(BASE_LUFS + sec["target_db"], 1)
|
|
2531
|
-
sec["breath"] = {"calm": 2, "body": 1, "drive": 0, "climax": 0}[tier]
|
|
2532
|
-
present = {name: [name in s["layers"] for s in secs] for name in LAYER_ORDER}
|
|
2533
|
-
last_si = len(secs) - 1
|
|
2534
|
-
|
|
2535
|
-
master = np.zeros((n, 2), np.float32)
|
|
2536
|
-
|
|
2537
|
-
# ---- 2) 鼓:按 style["drums"][tier] 的 kick 落点 / hat 步长 / clap ----
|
|
2538
|
-
hits: list = []
|
|
2539
|
-
drums = _Bus(n, sr)
|
|
2540
|
-
drum_pres = [bool(s["layers"].count("kick") + s["layers"].count("hat")) for s in secs]
|
|
2541
|
-
for si, sec in enumerate(secs):
|
|
2542
|
-
e = sec["energy"]
|
|
2543
|
-
tier = sec["tier"]
|
|
2544
|
-
cfg = pack["drums"][tier]
|
|
2545
|
-
has_kick = "kick" in sec["layers"]
|
|
2546
|
-
has_hat = "hat" in sec["layers"]
|
|
2547
|
-
if not has_kick and not has_hat:
|
|
2548
|
-
continue
|
|
2549
|
-
nb = len(sec["bars"])
|
|
2550
|
-
bm = sec["breath"]
|
|
2551
|
-
kick_db = -12.0 + 7.5 * e + (1.0 if tier == "climax" else 0.0)
|
|
2552
|
-
hat_db = -20.0 + 8.0 * e - float(m["drum"]) * 4.0
|
|
2553
|
-
for j, bi in enumerate(sec["bars"]):
|
|
2554
|
-
t0 = bi * bar
|
|
2555
|
-
phrase_end = (j % ph == ph - 1) and nb >= ph
|
|
2556
|
-
fill_bar = (j == nb - 1) and (si < len(secs) - 1)
|
|
2557
|
-
breath = phrase_end and not fill_bar and bm > 0
|
|
2558
|
-
if has_kick:
|
|
2559
|
-
for kb in cfg.get("kick", ()):
|
|
2560
|
-
if (fill_bar and kb >= 3.0) or (breath and kb >= 3.0):
|
|
2561
|
-
continue
|
|
2562
|
-
accent = 1.5 if abs(kb) < 1e-9 else 0.0
|
|
2563
|
-
var = bank.kicks[(bi * 4 + int(round(kb))) % len(bank.kicks)]
|
|
2564
|
-
drums.add(var, t0 + kb * beat, kick_db + accent)
|
|
2565
|
-
hits.append((t0 + kb * beat, 0.28 + 0.22 * e))
|
|
2566
|
-
# v0.7:指纹再塞一记 ghost kick(0.5 拍网格、不与现有落点重复;见 _styled_kick_ghost)
|
|
2567
|
-
if fp_s:
|
|
2568
|
-
ghost_pos = _styled_kick_ghost(cfg, fp_s, si, j, fill_bar, breath)
|
|
2569
|
-
if ghost_pos is not None:
|
|
2570
|
-
gidx = (bi * 4 + int(round(ghost_pos * 2)) + 2) % len(bank.kicks)
|
|
2571
|
-
drums.add(bank.kicks[gidx], t0 + ghost_pos * beat, kick_db - 5.5)
|
|
2572
|
-
hits.append((t0 + ghost_pos * beat, 0.12 + 0.12 * e))
|
|
2573
|
-
drum_ghosts += 1
|
|
2574
|
-
if has_hat and cfg.get("hat"):
|
|
2575
|
-
step = float(cfg["hat"])
|
|
2576
|
-
total = int(round(BEATS_PER_BAR / step))
|
|
2577
|
-
for k in range(total):
|
|
2578
|
-
base_pos = k * step
|
|
2579
|
-
pos = base_pos + _styled_swing_shift(swing, step, k)
|
|
2580
|
-
if fill_bar and pos >= 3.0 - 1e-9:
|
|
2581
|
-
continue
|
|
2582
|
-
if breath and pos >= (3.0 if bm >= 2 else 3.5) - 1e-9:
|
|
2583
|
-
continue
|
|
2584
|
-
on_beat = abs(base_pos - round(base_pos)) < 1e-9
|
|
2585
|
-
ghost = 0.0 if on_beat else -2.5
|
|
2586
|
-
pan = 0.3 if k % 2 == 0 else -0.3
|
|
2587
|
-
var = bank.hats[(bi * 7 + k) % len(bank.hats)]
|
|
2588
|
-
drums.add(var, t0 + pos * beat, hat_db + ghost, pan)
|
|
2589
|
-
if has_hat and e >= 0.50 and cfg.get("clap"):
|
|
2590
|
-
for cidx, cb in enumerate(cfg.get("clap", ())):
|
|
2591
|
-
if phrase_end and cb >= 3.0:
|
|
2592
|
-
continue
|
|
2593
|
-
pos = float(cb)
|
|
2594
|
-
if fp_s:
|
|
2595
|
-
shift = _styled_clap_shift(fp_s, si, j, cidx)
|
|
2596
|
-
if shift and 0.0 <= pos + shift < float(BEATS_PER_BAR):
|
|
2597
|
-
pos += shift
|
|
2598
|
-
drum_ghosts += 1
|
|
2599
|
-
drums.add(bank.claps[(bi * 2 + int(round(pos))) % len(bank.claps)],
|
|
2600
|
-
t0 + pos * beat, -16.0 + 4.0 * e - 1.5 * float(m["drum"]), 0.08)
|
|
2601
|
-
if tier == "climax" and has_hat:
|
|
2602
|
-
for pos in (0.5, 1.5, 2.5, 3.5):
|
|
2603
|
-
if phrase_end and pos >= 3.5:
|
|
2604
|
-
continue
|
|
2605
|
-
drums.add(bank.hats_open[(bi + int(pos)) % len(bank.hats_open)],
|
|
2606
|
-
t0 + pos * beat, -21.0 + 3.0 * e, -0.2)
|
|
2607
|
-
if si > 0 and tier in ("drive", "climax") and has_kick:
|
|
2608
|
-
drums.add(bank.crashes[si % len(bank.crashes)], sec["start"],
|
|
2609
|
-
-10.0 + 2.0 * (1.0 - e))
|
|
2610
|
-
drums.add(bank.kick_big, sec["start"], -9.0)
|
|
2611
|
-
hits.append((sec["start"], 0.5))
|
|
2612
|
-
if tier == "climax":
|
|
2613
|
-
for j, bi in enumerate(sec["bars"]):
|
|
2614
|
-
if j > 0 and j % 8 == 0:
|
|
2615
|
-
drums.add(bank.crashes[(si + j) % len(bank.crashes)], bi * bar, -17.0)
|
|
2616
|
-
drums.buf *= _styled_gate_env(n, secs, drum_pres, bar, sr)[:, None]
|
|
2617
|
-
master += drums.buf
|
|
2618
|
-
drums.clear()
|
|
2619
|
-
|
|
2620
|
-
# ---- 3) 贝斯:型(whole/half/off8/16/dub)+ 风格音色 + 侧链避让 ----
|
|
2621
|
-
bass = _Bus(n, sr)
|
|
2622
|
-
bass_voice = _check_voice("bass", pack["voices"]["bass"])
|
|
2623
|
-
for si, sec in enumerate(secs):
|
|
2624
|
-
if not present["bass"][si]:
|
|
2625
|
-
continue
|
|
2626
|
-
e = sec["energy"]
|
|
2627
|
-
cfg = pack["drums"][sec["tier"]]
|
|
2628
|
-
pat = cfg.get("bass", "none")
|
|
2629
|
-
if pat == "none":
|
|
2630
|
-
continue
|
|
2631
|
-
bdb = -14.0 + 5.0 * e
|
|
2632
|
-
bright = _clip01(0.2 + 0.65 * e + 0.5 * mood_bright)
|
|
2633
|
-
nb = len(sec["bars"])
|
|
2634
|
-
bm = sec["breath"]
|
|
2635
|
-
k = 0
|
|
2636
|
-
for j, bi in enumerate(sec["bars"]):
|
|
2637
|
-
t0 = bi * bar
|
|
2638
|
-
chord = kval["chords"][sec["chords"][j]]
|
|
2639
|
-
root = chord["root"]
|
|
2640
|
-
phrase_end = (j % ph == ph - 1) and nb >= ph
|
|
2641
|
-
breath = phrase_end and bm >= 2
|
|
2642
|
-
if pat == "whole":
|
|
2643
|
-
bass.add(_bass_texture(bank, bass_voice, root, bar * 0.92, bright), t0, bdb)
|
|
2644
|
-
elif pat == "half":
|
|
2645
|
-
for pos, dd in ((0.0, 1.8), (2.0, 1.8)):
|
|
2646
|
-
if breath and pos >= 3.0:
|
|
2647
|
-
continue
|
|
2648
|
-
bass.add(_bass_texture(bank, bass_voice, root, dd * beat, bright),
|
|
2649
|
-
t0 + pos * beat, bdb)
|
|
2650
|
-
elif pat == "off8":
|
|
2651
|
-
for pos in (0.5, 1.5, 2.5, 3.5):
|
|
2652
|
-
if breath and pos >= 3.0:
|
|
2653
|
-
continue
|
|
2654
|
-
nt = _up(root, 1) if (k % 7 == 3) else root
|
|
2655
|
-
bass.add(_bass_texture(bank, bass_voice, nt, 0.42 * beat, bright),
|
|
2656
|
-
t0 + pos * beat, bdb)
|
|
2657
|
-
k += 1
|
|
2658
|
-
elif pat == "dub":
|
|
2659
|
-
bass.add(_bass_texture(bank, bass_voice, root, bar * 0.55, bright), t0, bdb - 1.0)
|
|
2660
|
-
if not breath:
|
|
2661
|
-
bass.add(_bass_texture(bank, bass_voice, _up(root, 1), bar * 0.30, bright),
|
|
2662
|
-
t0 + 2.5 * beat, bdb - 3.0)
|
|
2663
|
-
else: # "16":十六分切分
|
|
2664
|
-
for b in range(BEATS_PER_BAR):
|
|
2665
|
-
for off in (0.25, 0.5, 0.75):
|
|
2666
|
-
pos = b + off
|
|
2667
|
-
if breath and pos >= 3.0:
|
|
2668
|
-
continue
|
|
2669
|
-
nt = _up(root, 1) if (k % 11 == 7) else root
|
|
2670
|
-
bass.add(_bass_texture(bank, bass_voice, nt, 0.2 * beat, bright),
|
|
2671
|
-
t0 + pos * beat, bdb)
|
|
2672
|
-
k += 1
|
|
2673
|
-
bass.buf *= sc._duck_env(n, hits, depth=float(pack["duck"]["bass"]), tau=0.15)[:, None]
|
|
2674
|
-
bass.buf *= _styled_gate_env(n, secs, present["bass"], bar, sr)[:, None]
|
|
2675
|
-
master += bass.buf
|
|
2676
|
-
bass.clear()
|
|
2677
|
-
|
|
2678
|
-
# ---- 4) 铺底 pad / strings(受底鼓轻压;段落首小节慢起音;shine 叠高八度) ----
|
|
2679
|
-
pads = _Bus(n, sr)
|
|
2680
|
-
pad_voice = _check_voice("pad", pack["voices"]["pad"])
|
|
2681
|
-
attack_first = {"calm": 1.2, "body": 0.5, "drive": 0.24, "climax": 0.14}
|
|
2682
|
-
attack_next = {"calm": 0.42, "body": 0.14, "drive": 0.07, "climax": 0.05}
|
|
2683
|
-
for si, sec in enumerate(secs):
|
|
2684
|
-
if not present["pad"][si]:
|
|
2685
|
-
continue
|
|
2686
|
-
e = sec["energy"]
|
|
2687
|
-
tier = sec["tier"]
|
|
2688
|
-
pdb = -20.0 + 7.0 * e + 0.8 * mood_bright
|
|
2689
|
-
bright = _clip01(0.15 + 0.75 * e + mood_bright)
|
|
2690
|
-
for j, bi in enumerate(sec["bars"]):
|
|
2691
|
-
chord = kval["chords"][sec["chords"][j]]
|
|
2692
|
-
attack = attack_first[tier] if j == 0 else attack_next[tier]
|
|
2693
|
-
if pad_voice == "strings":
|
|
2694
|
-
tex = bank.strings(chord, bright, attack)
|
|
2695
|
-
else:
|
|
2696
|
-
tex = bank.pad(chord, bright, attack)
|
|
2697
|
-
pads.add(tex, bi * bar, pdb)
|
|
2698
|
-
if present["shine"][si]:
|
|
2699
|
-
pads.add(bank.pad(chord, _clip01(bright - 0.1), attack, octave=1),
|
|
2700
|
-
bi * bar, pdb - 6.0)
|
|
2701
|
-
pads.buf *= sc._duck_env(n, hits, depth=float(pack["duck"]["pad"]), tau=0.18)[:, None]
|
|
2702
|
-
pads.buf *= _styled_gate_env(n, secs, present["pad"], bar, sr)[:, None]
|
|
2703
|
-
master += pads.buf
|
|
2704
|
-
pads.clear()
|
|
2705
|
-
|
|
2706
|
-
# ---- 5) 琶音:步长来自 drums[arp],音色来自 voices[arp],带摇摆 ----
|
|
2707
|
-
keys = _Bus(n, sr)
|
|
2708
|
-
arp_voice = _check_voice("arp", pack["voices"]["arp"])
|
|
2709
|
-
for si, sec in enumerate(secs):
|
|
2710
|
-
if not present["arp"][si]:
|
|
2711
|
-
continue
|
|
2712
|
-
cfg = pack["drums"][sec["tier"]]
|
|
2713
|
-
step = cfg.get("arp")
|
|
2714
|
-
if not step:
|
|
2715
|
-
continue
|
|
2716
|
-
step = float(step)
|
|
2717
|
-
e = sec["energy"]
|
|
2718
|
-
adb = -25.0 + 8.0 * e + 0.5 * mood_bright
|
|
2719
|
-
bright = _clip01(0.25 + 0.6 * e + mood_bright)
|
|
2720
|
-
nb = len(sec["bars"])
|
|
2721
|
-
bm = sec["breath"]
|
|
2722
|
-
for j, bi in enumerate(sec["bars"]):
|
|
2723
|
-
t0 = bi * bar
|
|
2724
|
-
chord = kval["chords"][sec["chords"][j]]
|
|
2725
|
-
seq = [_up(nt, 2) for nt in chord["notes"]]
|
|
2726
|
-
seq = seq + seq[1:3][::-1]
|
|
2727
|
-
total = int(round(BEATS_PER_BAR / step))
|
|
2728
|
-
phrase_end = (j % ph == ph - 1) and nb >= ph
|
|
2729
|
-
breath = phrase_end and bm > 0
|
|
2730
|
-
for k in range(total):
|
|
2731
|
-
pos = k * step + _styled_swing_shift(swing, step, k)
|
|
2732
|
-
if breath and pos >= 3.0 - 1e-9:
|
|
2733
|
-
continue
|
|
2734
|
-
nt = seq[k % len(seq)]
|
|
2735
|
-
pan = 0.35 * math.sin(0.7 * k + 0.3 * j)
|
|
2736
|
-
keys.add(_arp_texture(bank, arp_voice, nt, bright, 0.3),
|
|
2737
|
-
t0 + pos * beat, adb, pan)
|
|
2738
|
-
keys.buf *= _styled_gate_env(n, secs, present["arp"], bar, sr)[:, None]
|
|
2739
|
-
master += keys.buf
|
|
2740
|
-
sc._add_delayed(master, keys.buf, float(space["delay_beats"]) * beat,
|
|
2741
|
-
float(space["delay_db"]), taps=int(space["taps"]), sr=sr)
|
|
2742
|
-
keys.clear()
|
|
2743
|
-
|
|
2744
|
-
# ---- 6) 主奏:motif + lead_cycle 每乐句轮换;片尾用 end 动机 ----
|
|
2745
|
-
lead = _Bus(n, sr)
|
|
2746
|
-
cycle = tuple(pack["lead_cycle"])
|
|
2747
|
-
phrase_i = 0
|
|
2748
|
-
lead_pres = list(present["melody"])
|
|
2749
|
-
if last_si >= 0 and secs[last_si]["energy"] >= 0.22:
|
|
2750
|
-
lead_pres[last_si] = True
|
|
2751
|
-
for si, sec in enumerate(secs):
|
|
2752
|
-
e = sec["energy"]
|
|
2753
|
-
nb = len(sec["bars"])
|
|
2754
|
-
if nb < 2:
|
|
2755
|
-
continue
|
|
2756
|
-
bright = _clip01(0.4 + 0.5 * e + mood_bright)
|
|
2757
|
-
if present["melody"][si]:
|
|
2758
|
-
mel_db = -17.0 + 4.0 * e + float(m["lead_db"])
|
|
2759
|
-
for pi, first in enumerate(range(0, nb, ph)):
|
|
2760
|
-
is_last_phrase = (si == last_si and pi == (nb - 1) // ph)
|
|
2761
|
-
if is_last_phrase:
|
|
2762
|
-
kind, motif = "end", pack["motif"]["end"]
|
|
2763
|
-
elif phrase_i % 2 == 0:
|
|
2764
|
-
kind, motif = "q", pack["motif"]["q"]
|
|
2765
|
-
else:
|
|
2766
|
-
kind, motif = "a", pack["motif"]["a"]
|
|
2767
|
-
voice = cycle[phrase_i % len(cycle)]
|
|
2768
|
-
t0 = sec["bars"][first] * bar
|
|
2769
|
-
motif_shifts += _emit_styled_motif(
|
|
2770
|
-
lead, bank, motif, kval, voice, t0, beat, mel_db, bright,
|
|
2771
|
-
fingerprint=fingerprint, style=style, kind=kind, mode=mode)
|
|
2772
|
-
phrase_i += 1
|
|
2773
|
-
elif si == last_si and e >= 0.22:
|
|
2774
|
-
# 片尾安静段:用第一个主奏音色把结局动机答完
|
|
2775
|
-
t_last = sec["bars"][0] * bar + max(0.0, (nb - 2) * bar)
|
|
2776
|
-
motif_shifts += _emit_styled_motif(
|
|
2777
|
-
lead, bank, pack["motif"]["end"], kval, cycle[0],
|
|
2778
|
-
t_last, beat, -19.0 + float(m["lead_db"]), bright,
|
|
2779
|
-
fingerprint=fingerprint, style=style, kind="end", mode=mode)
|
|
2780
|
-
lead.buf *= _styled_gate_env(n, secs, lead_pres, bar, sr)[:, None]
|
|
2781
|
-
master += lead.buf
|
|
2782
|
-
sc._add_delayed(master, lead.buf, float(space["delay_beats"]) * beat * 0.85,
|
|
2783
|
-
float(space["delay_db"]) - 3.0,
|
|
2784
|
-
taps=max(1, int(space["taps"]) - 1), sr=sr)
|
|
2785
|
-
lead.clear()
|
|
2786
|
-
|
|
2787
|
-
# ---- 7) shine:高音闪光(只在最亮的段出现) ----
|
|
2788
|
-
shine = _Bus(n, sr)
|
|
2789
|
-
for si, sec in enumerate(secs):
|
|
2790
|
-
if not present["shine"][si]:
|
|
2791
|
-
continue
|
|
2792
|
-
e = sec["energy"]
|
|
2793
|
-
sdb = -30.0 + 6.0 * e + 0.6 * mood_bright
|
|
2794
|
-
for j, bi in enumerate(sec["bars"]):
|
|
2795
|
-
chord = kval["chords"][sec["chords"][j]]
|
|
2796
|
-
hi = [_up(nt, 2) for nt in chord["notes"][1:3]]
|
|
2797
|
-
for q, pos in enumerate((0.5, 2.75)):
|
|
2798
|
-
shine.add(bank.glock(hi[(bi + q) % len(hi)], 0.9), bi * bar + pos * beat,
|
|
2799
|
-
sdb, 0.4 if q == 0 else -0.4)
|
|
2800
|
-
shine.buf *= _styled_gate_env(n, secs, present["shine"], bar, sr)[:, None]
|
|
2801
|
-
master += shine.buf
|
|
2802
|
-
shine.clear()
|
|
2803
|
-
|
|
2804
|
-
# ---- 8) 过门与效果(每个内部交界一小节) ----
|
|
2805
|
-
fx = _Bus(n, sr)
|
|
2806
|
-
for ti, tr in enumerate(plan["transitions"]):
|
|
2807
|
-
fill_start = tr["t"] - bar
|
|
2808
|
-
nxt_e = secs[ti + 1]["energy"]
|
|
2809
|
-
prev_e = secs[ti]["energy"]
|
|
2810
|
-
sixteenth = 0.25 * beat
|
|
2811
|
-
for k in range(int(round(bar / sixteenth))):
|
|
2812
|
-
p = k / max(1.0, (bar / sixteenth) - 1.0)
|
|
2813
|
-
tom = bank.toms[min(3, int(p * 4))]
|
|
2814
|
-
fx.add(tom, fill_start + k * sixteenth,
|
|
2815
|
-
-18.0 + 6.0 * p + (1.5 if k % 2 == 0 else 0.0), -0.35 + 0.7 * p)
|
|
2816
|
-
if p >= 0.6:
|
|
2817
|
-
fx.add(bank.snares[k % len(bank.snares)], fill_start + k * sixteenth,
|
|
2818
|
-
-20.0 + 5.0 * p, 0.1)
|
|
2819
|
-
fx.add(bank.swell(bar, ti, "styled"), fill_start, -15.0 + 2.0 * nxt_e)
|
|
2820
|
-
if nxt_e > prev_e + 0.10:
|
|
2821
|
-
fx.add(bank.riser(bar * 0.92, ti, "styled"), fill_start + bar * 0.08,
|
|
2822
|
-
-14.0 + 2.0 * nxt_e)
|
|
2823
|
-
if nxt_e >= 0.72:
|
|
2824
|
-
fx.add(bank.sub_boom(2.2, 55.0, 30.0), tr["t"], -9.0 - 2.0 * (1.0 - nxt_e))
|
|
2825
|
-
fx.add(bank.crashes[ti % len(bank.crashes)], tr["t"], -11.0)
|
|
2826
|
-
if nxt_e < prev_e - 0.15:
|
|
2827
|
-
fx.add(bank.crashes[(ti + 1) % len(bank.crashes)], tr["t"], -20.0)
|
|
2828
|
-
master += fx.buf
|
|
2829
|
-
fx.clear()
|
|
2830
|
-
|
|
2831
|
-
# ---- 9) 自动电平(段落间能量 -> dB 曲线)/ 吸气 / 去直流 / 淡入淡出 / 峰值 ----
|
|
2832
|
-
levels = sc._level_match(master, plan)
|
|
2833
|
-
sc._apply_sucks(master, plan["sucks"], sr)
|
|
2834
|
-
sc._de_dc(master)
|
|
2835
|
-
sc._apply_fades(master)
|
|
2836
|
-
peak = float(np.max(np.abs(master))) if n else 0.0
|
|
2837
|
-
if peak > 1e-9:
|
|
2838
|
-
master *= np.float32(_db_amp(TARGET_PEAK_DB) / peak)
|
|
2839
|
-
peak_db = float(20.0 * math.log10(max(float(np.max(np.abs(master))), 1e-12)))
|
|
2840
|
-
lufs = _approx_lufs(master, sr)
|
|
2841
|
-
dc = float(np.max(np.abs(master.mean(axis=0)))) if n else 0.0
|
|
2842
|
-
centroid = spectral_centroid(master, sr)
|
|
2843
|
-
|
|
2844
|
-
# ---- 10) 报告(version 2;字段与 legacy report 兼容,CLI 复用同一个格式化函数) ----
|
|
2845
|
-
rep = {
|
|
2846
|
-
"version": 2,
|
|
2847
|
-
"generator": "scorekit",
|
|
2848
|
-
"style": style,
|
|
2849
|
-
"style_label": str(pack.get("label", style)),
|
|
2850
|
-
"mood": mood,
|
|
2851
|
-
"bpm": round(float(bpm), 4),
|
|
2852
|
-
"bpm_hint": list(pack["bpm_hint"]),
|
|
2853
|
-
"key": key,
|
|
2854
|
-
"key_name": kval["name"],
|
|
2855
|
-
"key_in_style_pool": key in tuple(pack["keys"]),
|
|
2856
|
-
"sr": sr,
|
|
2857
|
-
"duration": round(dur, 3),
|
|
2858
|
-
"seed": int(sc.seed),
|
|
2859
|
-
"seed_effective": int(seed_eff),
|
|
2860
|
-
"fingerprint": "" if fingerprint is None else str(fingerprint),
|
|
2861
|
-
# v0.7:指纹变奏的实际发生数(审计用;无指纹时三项目全 0)
|
|
2862
|
-
"variation": {"chord_swaps": int(chord_swaps),
|
|
2863
|
-
"motif_shifts": int(motif_shifts),
|
|
2864
|
-
"drum_ghosts": int(drum_ghosts)},
|
|
2865
|
-
"bar_s": round(bar, 4),
|
|
2866
|
-
"beat_s": round(beat, 4),
|
|
2867
|
-
"phrase_bars": ph,
|
|
2868
|
-
"since": "v0.5",
|
|
2869
|
-
"sections": [
|
|
2870
|
-
{"name": s["name"], "start": round(s["start"], 3), "end": round(s["end"], 3),
|
|
2871
|
-
"energy": round(s["energy"], 3), "tier": s["tier"], "bars": len(s["bars"]),
|
|
2872
|
-
"target_db": round(s["target_db"], 2), "target_lufs": round(s["target_lufs"], 1),
|
|
2873
|
-
"layers": list(s["layers"]), "chords": list(s["chords"])}
|
|
2874
|
-
for s in secs
|
|
2875
|
-
],
|
|
2876
|
-
"transitions": [dict(t) for t in plan["transitions"]],
|
|
2877
|
-
"sucks": [dict(d) for d in plan["sucks"]],
|
|
2878
|
-
"beats": list(plan["beats"]),
|
|
2879
|
-
"peak_dbfs": round(peak_db, 2),
|
|
2880
|
-
"lufs_approx": round(lufs, 1),
|
|
2881
|
-
"dc_max": round(dc, 9),
|
|
2882
|
-
"spectral_centroid_hz": round(centroid, 1),
|
|
2883
|
-
"render_s": round(time.perf_counter() - t_start, 3),
|
|
2884
|
-
"measured_sections": [
|
|
2885
|
-
{"name": s["name"], "start": round(s["start"], 3), "end": round(s["end"], 3),
|
|
2886
|
-
"energy": round(s["energy"], 3), "gain_db": round(lv["gain"], 2),
|
|
2887
|
-
"measured_db": round(lv["after"], 2)}
|
|
2888
|
-
for s, lv in zip(secs, levels)
|
|
2889
|
-
],
|
|
2890
|
-
}
|
|
2891
|
-
return master, rep
|
|
2892
1433
|
|
|
2893
1434
|
|
|
2894
1435
|
def spectral_centroid(x, sr: int = SR) -> float:
|
|
@@ -2920,116 +1461,7 @@ def spectral_centroid(x, sr: int = SR) -> float:
|
|
|
2920
1461
|
return float(acc_m / acc_p)
|
|
2921
1462
|
|
|
2922
1463
|
|
|
2923
|
-
def _music_style_lint() -> list:
|
|
2924
|
-
"""风格表体检:返回问题列表(空 = 过)。selftest 第 17 条调用。"""
|
|
2925
|
-
bad: list = []
|
|
2926
|
-
need = ("bpm_hint", "keys", "progressions", "motif", "layers", "voices",
|
|
2927
|
-
"lead_cycle", "drums", "space", "phrase_bars", "duck",
|
|
2928
|
-
"level_span_db", "swing")
|
|
2929
|
-
for sid, pack in MUSIC_STYLES.items():
|
|
2930
|
-
missing = [fld for fld in need if fld not in pack]
|
|
2931
|
-
if missing:
|
|
2932
|
-
bad.append(f"{sid}: 缺字段 {'/'.join(missing)}")
|
|
2933
|
-
continue
|
|
2934
|
-
if not pack["bpm_hint"]:
|
|
2935
|
-
bad.append(f"{sid}: bpm_hint 为空")
|
|
2936
|
-
for kk in pack["keys"]:
|
|
2937
|
-
if kk not in KEYS_ALL:
|
|
2938
|
-
bad.append(f"{sid}: 调 {kk} 不在 KEYS_ALL")
|
|
2939
|
-
for tier in ("calm", "body", "drive"):
|
|
2940
|
-
if tier not in pack["progressions"]:
|
|
2941
|
-
bad.append(f"{sid}: progressions 缺 {tier}")
|
|
2942
|
-
continue
|
|
2943
|
-
for deg in pack["progressions"][tier]:
|
|
2944
|
-
if deg not in _DEGREE_BY_NAME:
|
|
2945
|
-
bad.append(f"{sid}.{tier}: 级数 {deg} 不存在")
|
|
2946
|
-
for mt in ("q", "a", "end"):
|
|
2947
|
-
if mt not in pack["motif"]:
|
|
2948
|
-
bad.append(f"{sid}: motif 缺 {mt}")
|
|
2949
|
-
continue
|
|
2950
|
-
for idx, ev in enumerate(pack["motif"][mt]):
|
|
2951
|
-
if len(ev) != 3:
|
|
2952
|
-
bad.append(f"{sid}.{mt}[{idx}]: 必须是 (拍, 级数, 时值拍)")
|
|
2953
|
-
continue
|
|
2954
|
-
if str(ev[1]) not in _DEGREE_BY_NAME:
|
|
2955
|
-
bad.append(f"{sid}.{mt}[{idx}]: 级数 {ev[1]} 不存在")
|
|
2956
|
-
if float(ev[2]) <= 0.0:
|
|
2957
|
-
bad.append(f"{sid}.{mt}[{idx}]: 时值必须 > 0")
|
|
2958
|
-
for slot in ("pad", "bass", "arp", "lead"):
|
|
2959
|
-
if pack["voices"].get(slot) not in _STYLE_VOICES[slot]:
|
|
2960
|
-
bad.append(f"{sid}.voices.{slot}={pack['voices'].get(slot)!r} 未知")
|
|
2961
|
-
for voice in pack["lead_cycle"]:
|
|
2962
|
-
if voice not in _STYLE_VOICES["lead"]:
|
|
2963
|
-
bad.append(f"{sid}.lead_cycle: 音色 {voice!r} 未知")
|
|
2964
|
-
for tier in ("calm", "body", "drive", "climax"):
|
|
2965
|
-
cfg = pack["drums"].get(tier) or {}
|
|
2966
|
-
if not cfg:
|
|
2967
|
-
bad.append(f"{sid}.drums: 缺 {tier}")
|
|
2968
|
-
continue
|
|
2969
|
-
for kb in cfg.get("kick", ()):
|
|
2970
|
-
if not (0.0 <= float(kb) < BEATS_PER_BAR):
|
|
2971
|
-
bad.append(f"{sid}.drums.{tier}: kick {kb} 越界")
|
|
2972
|
-
for cb in cfg.get("clap", ()):
|
|
2973
|
-
if not (0.0 <= float(cb) < BEATS_PER_BAR):
|
|
2974
|
-
bad.append(f"{sid}.drums.{tier}: clap {cb} 越界")
|
|
2975
|
-
hs = cfg.get("hat", None)
|
|
2976
|
-
if hs is not None and not (0.20 <= float(hs) <= 2.0):
|
|
2977
|
-
bad.append(f"{sid}.drums.{tier}: hat {hs} 不合理")
|
|
2978
|
-
if cfg.get("bass", "none") not in ("none", "whole", "half", "off8", "16", "dub"):
|
|
2979
|
-
bad.append(f"{sid}.drums.{tier}: bass 型 {cfg.get('bass')!r} 未知")
|
|
2980
|
-
asp = cfg.get("arp", None)
|
|
2981
|
-
if asp is not None and not (0.2 <= float(asp) <= 2.0):
|
|
2982
|
-
bad.append(f"{sid}.drums.{tier}: arp {asp} 不合理")
|
|
2983
|
-
if int(pack["phrase_bars"]) not in (4, 8):
|
|
2984
|
-
bad.append(f"{sid}: phrase_bars 只能是 4/8")
|
|
2985
|
-
if not (float(pack["level_span_db"]) > 0.0):
|
|
2986
|
-
bad.append(f"{sid}: level_span_db 必须 > 0")
|
|
2987
|
-
if not (0.0 <= float(pack["swing"]) <= 0.2):
|
|
2988
|
-
bad.append(f"{sid}: swing 必须 0~0.2")
|
|
2989
|
-
for fld in ("bass", "pad"):
|
|
2990
|
-
d = float(pack["duck"].get(fld, 0.0))
|
|
2991
|
-
if not (0.0 <= d < 0.9):
|
|
2992
|
-
bad.append(f"{sid}: duck.{fld}={d} 不合理")
|
|
2993
|
-
if float(pack["layers"].get("pad", 1.0)) > 1e-9:
|
|
2994
|
-
bad.append(f"{sid}: layers.pad 必须是 0.0")
|
|
2995
|
-
for nm in LAYER_ORDER:
|
|
2996
|
-
if nm not in pack["layers"]:
|
|
2997
|
-
bad.append(f"{sid}: layers 缺 {nm}")
|
|
2998
|
-
sp = pack["space"]
|
|
2999
|
-
if not (0.0 < float(sp.get("delay_beats", 0.0)) <= 4.0):
|
|
3000
|
-
bad.append(f"{sid}: space.delay_beats 必须 0~4 拍")
|
|
3001
|
-
if int(sp.get("taps", 0)) < 1:
|
|
3002
|
-
bad.append(f"{sid}: space.taps 必须 >= 1")
|
|
3003
|
-
return bad
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
def _stylekit_crosscheck() -> list:
|
|
3007
|
-
"""同目录有 stylekit.py 时交叉核对风格 id 与调池;没有就静默跳过(不算失败)。"""
|
|
3008
|
-
try:
|
|
3009
|
-
import stylekit as ST # noqa: F401
|
|
3010
|
-
except Exception: # noqa: BLE001
|
|
3011
|
-
return []
|
|
3012
|
-
bad: list = []
|
|
3013
|
-
for sid in MUSIC_STYLE_IDS:
|
|
3014
|
-
if sid == LEGACY_MUSIC_STYLE:
|
|
3015
|
-
continue
|
|
3016
|
-
if sid not in ST.STYLES:
|
|
3017
|
-
bad.append(f"stylekit 缺风格 {sid}")
|
|
3018
|
-
continue
|
|
3019
|
-
want = tuple(ST.STYLES[sid]["music"]["keys"])
|
|
3020
|
-
got = tuple(MUSIC_STYLES[sid]["keys"])
|
|
3021
|
-
if want != got:
|
|
3022
|
-
bad.append(f"{sid}: 调池不一致 {got} != {want}")
|
|
3023
|
-
return bad
|
|
3024
|
-
|
|
3025
1464
|
|
|
3026
|
-
def _stylekit_available() -> bool:
|
|
3027
|
-
"""同目录是否有可导入的 stylekit(没有就跳过交叉核对,不判失败)。"""
|
|
3028
|
-
try:
|
|
3029
|
-
import stylekit # noqa: F401
|
|
3030
|
-
return True
|
|
3031
|
-
except Exception: # noqa: BLE001
|
|
3032
|
-
return False
|
|
3033
1465
|
|
|
3034
1466
|
# ============================================================================
|
|
3035
1467
|
# 模块级便捷函数
|
|
@@ -3107,16 +1539,21 @@ def render(duration, bpm: float = 120.0, key: str = "Dm", anchors=None, sections
|
|
|
3107
1539
|
anchors:timeline.json 的 [{id,kind,start,end}](自动按 kind 映射能量分段);
|
|
3108
1540
|
sections:显式段落(绝对时间或 at 引用),在 anchors 之后追加/覆盖空隙。
|
|
3109
1541
|
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
1542
|
+
没有配乐风格层。style 只接受 None / "" / "legacy"(都走本模块的确定性路径);
|
|
1543
|
+
其它风格 id 一律抛 ValueError 并指向 musicgen.py —— 本仓库不提供配乐模板。
|
|
1544
|
+
mood / fingerprint / music_plan / pace 是为了签名兼容而保留的**废参数**:
|
|
1545
|
+
它们只影响风格层,风格层不存在就一律忽略(不要靠它们调音色,那是模板思维)。
|
|
3113
1546
|
"""
|
|
3114
|
-
sid =
|
|
3115
|
-
if sid
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
1547
|
+
sid = "" if style is None else str(style).strip().lower()
|
|
1548
|
+
if sid not in ("", "legacy"):
|
|
1549
|
+
raise ValueError(
|
|
1550
|
+
"scorekit 的配乐风格层已删除(零模板):style=%r 不再可用。\n"
|
|
1551
|
+
" 需要现场编排的配乐请用 musicgen.py:\n"
|
|
1552
|
+
" import musicgen as MG\n"
|
|
1553
|
+
" samples, report = MG.render(dur, bpm=110, key=%r, scale=\"minor\",\n"
|
|
1554
|
+
" mood=%r, seed=7)\n"
|
|
1555
|
+
" (成片链路见 render.py 的 _score_with_musicgen)" % (style, key, mood))
|
|
1556
|
+
# ---- legacy 逐样本兼容是硬契约:这段代码别改任何东西 ----
|
|
3120
1557
|
sc = Score(duration, bpm=bpm, key=key, seed=seed)
|
|
3121
1558
|
for s in _merge_sections(anchors, sections, float(duration)):
|
|
3122
1559
|
sc.section(s["name"], s["start"], s["end"], s["energy"])
|
|
@@ -3431,16 +1868,7 @@ def selftest() -> int:
|
|
|
3431
1868
|
check("16. 集成形态:anchors+sections 同起点覆盖 / 无 end 自动补到片尾", ok_it,
|
|
3432
1869
|
" | ".join(f"{b['name']}:{b['start']}-{b['end']}:e{b['energy']}" for b in merged))
|
|
3433
1870
|
|
|
3434
|
-
# 17)
|
|
3435
|
-
lint_bad = _music_style_lint()
|
|
3436
|
-
cross_bad = _stylekit_crosscheck()
|
|
3437
|
-
check("17. 配乐风格表完整(9 风格 / 级数存在 / 调池在 KEYS_ALL)",
|
|
3438
|
-
not (lint_bad or cross_bad),
|
|
3439
|
-
"; ".join((lint_bad + cross_bad)[:3]) if (lint_bad or cross_bad)
|
|
3440
|
-
else f"{len(MUSIC_STYLES)} 风格,{len(_DEGREE_BY_NAME)} 个级数"
|
|
3441
|
-
+ (";stylekit 调池一致" if _stylekit_available() else ";无 stylekit 可交叉核对"))
|
|
3442
|
-
|
|
3443
|
-
# 18) v0.5 大调 & 七和弦表(KEYS 仍是 v0.4 的 4 个小调)
|
|
1871
|
+
# 17) 大调 & 七和弦表(KEYS 仍是 4 个小调)
|
|
3444
1872
|
bad_major = []
|
|
3445
1873
|
for kname, kv in KEYS_MAJOR.items():
|
|
3446
1874
|
for deg in ("I", "ii", "iii", "IV", "V", "vi", "vii", "sus4",
|
|
@@ -3453,93 +1881,11 @@ def selftest() -> int:
|
|
|
3453
1881
|
bad_major.append(f"{kname}: 非法音名 {nt!r}")
|
|
3454
1882
|
ok_major = ((not bad_major) and set(KEYS) == {"Dm", "Am", "Cm", "Em"}
|
|
3455
1883
|
and len(KEYS_ALL) == len(KEYS) + len(KEYS_MAJOR) == 10)
|
|
3456
|
-
check("
|
|
1884
|
+
check("17. 大调 6 个 & 七和弦级数(KEYS 仍是 4 个小调)", ok_major,
|
|
3457
1885
|
"; ".join(bad_major[:3]) if bad_major
|
|
3458
1886
|
else f"{len(KEYS_ALL)} 个调,{len(KEYS_ALL['C']['chords'])} 个级数/调")
|
|
3459
1887
|
|
|
3460
|
-
#
|
|
3461
|
-
t_styled = time.perf_counter()
|
|
3462
|
-
style_ids = [sid for sid in MUSIC_STYLE_IDS if sid != LEGACY_MUSIC_STYLE]
|
|
3463
|
-
pcm = {}
|
|
3464
|
-
peaks = {}
|
|
3465
|
-
bad_style = []
|
|
3466
|
-
for sid in style_ids:
|
|
3467
|
-
pk = MUSIC_STYLES[sid]
|
|
3468
|
-
hint = pk["bpm_hint"]
|
|
3469
|
-
bpm_s = float(hint[len(hint) // 2])
|
|
3470
|
-
key_s = str(pk["keys"][0])
|
|
3471
|
-
try:
|
|
3472
|
-
x1, r1 = render_styled(6.0, style=sid, bpm=bpm_s, key=key_s, seed=7)
|
|
3473
|
-
x2, _r2 = render_styled(6.0, style=sid, bpm=bpm_s, key=key_s, seed=7)
|
|
3474
|
-
except Exception as exc: # 自测里也要报原因,不吞异常
|
|
3475
|
-
bad_style.append(f"{sid}: {type(exc).__name__}: {exc}")
|
|
3476
|
-
continue
|
|
3477
|
-
want_n = int(math.ceil(6.0 * SR - 1e-9))
|
|
3478
|
-
if x1.shape != (want_n, 2) or not bool(np.all(np.isfinite(x1))):
|
|
3479
|
-
bad_style.append(f"{sid}: shape={x1.shape} 或含非有限值")
|
|
3480
|
-
if not np.array_equal(x1, x2):
|
|
3481
|
-
bad_style.append(f"{sid}: 同 seed 两次不一致")
|
|
3482
|
-
peaks[sid] = 20.0 * math.log10(max(float(np.max(np.abs(x1))), 1e-12))
|
|
3483
|
-
if not (-3.5 <= peaks[sid] <= -1.0):
|
|
3484
|
-
bad_style.append(f"{sid}: 峰值 {peaks[sid]:.2f} dBFS 越界")
|
|
3485
|
-
if r1.get("version") != 2 or r1.get("style") != sid or "sections" not in r1:
|
|
3486
|
-
bad_style.append(f"{sid}: report 结构不对")
|
|
3487
|
-
pcm[sid] = x1
|
|
3488
|
-
same_pairs = []
|
|
3489
|
-
ids_ok = list(pcm)
|
|
3490
|
-
for i in range(len(ids_ok)):
|
|
3491
|
-
for j in range(i + 1, len(ids_ok)):
|
|
3492
|
-
if np.array_equal(pcm[ids_ok[i]], pcm[ids_ok[j]]):
|
|
3493
|
-
same_pairs.append(f"{ids_ok[i]}=={ids_ok[j]}")
|
|
3494
|
-
cen = {sid: spectral_centroid(x, SR) for sid, x in pcm.items()}
|
|
3495
|
-
spread = (max(cen.values()) - min(cen.values())) if cen else 0.0
|
|
3496
|
-
|
|
3497
|
-
# 19b) stylekit 的 music_plan 优先级:plan 里的 bpm/key/mood/seed 必须盖过显式参数
|
|
3498
|
-
plan_used = {"bpm": 123.0, "key": "A", "mood": "uplifting", "seed": 42,
|
|
3499
|
-
"seed_effective": 424242}
|
|
3500
|
-
try:
|
|
3501
|
-
_xp, rp = render_styled(4.0, style="vivid", bpm=90.0, key="D", mood="calm", seed=1,
|
|
3502
|
-
music_plan={"music": plan_used})
|
|
3503
|
-
ok_plan = (rp.get("bpm") == 123.0 and rp.get("key") == "A"
|
|
3504
|
-
and rp.get("mood") == "uplifting" and rp.get("seed_effective") == 424242)
|
|
3505
|
-
plan_detail = (f"plan -> {rp.get('bpm')}/{rp.get('key')}/{rp.get('mood')}/"
|
|
3506
|
-
f"seed {rp.get('seed_effective')}")
|
|
3507
|
-
except Exception as exc: # 自测里也要报原因
|
|
3508
|
-
ok_plan = False
|
|
3509
|
-
plan_detail = f"{type(exc).__name__}: {exc}"
|
|
3510
|
-
|
|
3511
|
-
# 19c) pace:min_dur+hold 只合并过碎段落,不改片长 / 时间轴
|
|
3512
|
-
stale_blocks = [
|
|
3513
|
-
{"name": "碎1", "start": 0.0, "end": 2.0, "energy": 0.3},
|
|
3514
|
-
{"name": "碎2", "start": 2.0, "end": 4.0, "energy": 0.6},
|
|
3515
|
-
{"name": "长", "start": 4.0, "end": 12.0, "energy": 0.5},
|
|
3516
|
-
]
|
|
3517
|
-
paced = _apply_pace(stale_blocks, {"min_dur": 3.2, "hold": 0.2})
|
|
3518
|
-
ok_pace = (len(paced) == 2 and abs(paced[0]["end"] - 4.0) < 1e-9
|
|
3519
|
-
and abs(paced[0]["energy"] - 0.6) < 1e-9
|
|
3520
|
-
and abs(paced[-1]["start"] - 4.0) < 1e-9
|
|
3521
|
-
and abs(paced[-1]["end"] - 12.0) < 1e-9)
|
|
3522
|
-
try:
|
|
3523
|
-
xpace, rpace = render_styled(
|
|
3524
|
-
6.0, style="vivid", bpm=126, key="D", seed=7,
|
|
3525
|
-
anchors=[{"id": "p%d" % i, "kind": "body", "start": i * 0.75,
|
|
3526
|
-
"end": (i + 1) * 0.75} for i in range(8)],
|
|
3527
|
-
pace={"min_dur": 3.0, "hold": 0.30})
|
|
3528
|
-
ok_pace = ok_pace and xpace.shape == (int(math.ceil(6.0 * SR - 1e-9)), 2)
|
|
3529
|
-
pace_detail = f"索引 {len(paced)} 段;6 s 片仍 {xpace.shape[0]} 采样,{len(rpace['sections'])} 段"
|
|
3530
|
-
except Exception as exc:
|
|
3531
|
-
ok_pace = False
|
|
3532
|
-
pace_detail = f"{type(exc).__name__}: {exc}"
|
|
3533
|
-
|
|
3534
|
-
ok_styled = ((not bad_style) and (not same_pairs) and len(pcm) == len(style_ids)
|
|
3535
|
-
and spread >= 100.0 and ok_plan and ok_pace)
|
|
3536
|
-
check("19. 8 风格各渲 6 s:时长/有限值/峰值/确定性/两两 PCM 不全等", ok_styled,
|
|
3537
|
-
("; ".join((bad_style + same_pairs)[:3]) if (bad_style or same_pairs)
|
|
3538
|
-
else f"{len(pcm)} 风格,{time.perf_counter() - t_styled:.1f} s;"
|
|
3539
|
-
f"频谱质心 {min(cen.values()):.0f}~{max(cen.values()):.0f} Hz(跨度 {spread:.0f});"
|
|
3540
|
-
f"{plan_detail};{pace_detail}"))
|
|
3541
|
-
|
|
3542
|
-
# 20) v0.5 legacy 兼容:style=None / "legacy" / 任意 mood 都走 v0.4 原路径,逐样本一致
|
|
1888
|
+
# 18) legacy 兼容:style=None / "legacy" / 任意 mood 都走同一条确定性路径,逐样本一致
|
|
3543
1889
|
xn, rn = render(6.0, bpm=120, key="Dm", seed=7)
|
|
3544
1890
|
xl, rl = render(6.0, bpm=120, key="Dm", seed=7, style="legacy")
|
|
3545
1891
|
xm, _rm = render(6.0, bpm=120, key="Dm", seed=7, style=None, mood="driving",
|
|
@@ -3548,7 +1894,7 @@ def selftest() -> int:
|
|
|
3548
1894
|
rl_c = {k: v for k, v in rl.items() if k != "render_s"}
|
|
3549
1895
|
ok_legacy = (np.array_equal(xn, xl) and np.array_equal(xn, xm) and rn_c == rl_c
|
|
3550
1896
|
and rn.get("version") == 1)
|
|
3551
|
-
check("
|
|
1897
|
+
check("18. legacy 兼容:style=None / 'legacy' / 任意 mood+fingerprint 逐样本一致", ok_legacy,
|
|
3552
1898
|
f"shape={xn.shape},version={rn.get('version')}")
|
|
3553
1899
|
total = time.perf_counter() - t_all
|
|
3554
1900
|
passed = sum(1 for _, ok, _ in rows if ok)
|
|
@@ -3604,36 +1950,12 @@ def _parse_suck_list(items) -> list:
|
|
|
3604
1950
|
return out
|
|
3605
1951
|
|
|
3606
1952
|
|
|
3607
|
-
def _format_style_list() -> str:
|
|
3608
|
-
"""CLI --list-styles:列可用配乐风格(bpm_hint / 调池 / 主奏音色 / 乐句长度)。"""
|
|
3609
|
-
lines = ["可用配乐风格(--style;缺省 legacy = v0.4 逐样本兼容):"]
|
|
3610
|
-
for sid, pack in MUSIC_STYLES.items():
|
|
3611
|
-
lines.append(" %-10s %-8s bpm_hint %-16s 调池 %-20s 主奏 %-14s 乐句 %d 小节" % (
|
|
3612
|
-
sid, str(pack.get("label", "")),
|
|
3613
|
-
"/".join(str(b) for b in pack["bpm_hint"]),
|
|
3614
|
-
"/".join(str(k) for k in pack["keys"]),
|
|
3615
|
-
"/".join(str(v) for v in pack["lead_cycle"]),
|
|
3616
|
-
int(pack["phrase_bars"]),
|
|
3617
|
-
))
|
|
3618
|
-
lines.append(" 情绪 --mood:" + "/".join(MOOD_IDS))
|
|
3619
|
-
return "\n".join(lines)
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
1953
|
def _format_report(rep: dict, wav_path: Path | None, report_path: Path | None) -> str:
|
|
3623
1954
|
"""CLI 中文摘要。"""
|
|
3624
1955
|
lines = []
|
|
3625
1956
|
lines.append(f"[scorekit] {rep['duration']:g} s / {rep['bpm']:g} BPM / {rep['key']}({rep['key_name']})"
|
|
3626
1957
|
f" / 小节 {rep['bar_s']:g} s / seed {rep['seed']}")
|
|
3627
|
-
if rep.get("style"):
|
|
3628
|
-
lines.append(f" 配乐风格 {rep['style']}({rep.get('style_label', '')})"
|
|
3629
|
-
f" / 情绪 {rep.get('mood', 'steady')} / 有效 seed {rep.get('seed_effective', rep['seed'])}"
|
|
3630
|
-
f" / 频谱质心 {rep.get('spectral_centroid_hz', 0):.0f} Hz")
|
|
3631
1958
|
lines.append(f" 段落 {len(rep['sections'])} 个,交界 {len(rep['transitions'])} 处,吸气 {len(rep['sucks'])} 处")
|
|
3632
|
-
var = rep.get("variation") or {}
|
|
3633
|
-
if any(int(var.get(k, 0) or 0) for k in ("chord_swaps", "motif_shifts", "drum_ghosts")):
|
|
3634
|
-
lines.append(" 指纹变奏:和弦替换 %d 处 / 动机偏移 %d 音 / 鼓型 ghost %d 处"
|
|
3635
|
-
% (int(var.get("chord_swaps", 0)), int(var.get("motif_shifts", 0)),
|
|
3636
|
-
int(var.get("drum_ghosts", 0))))
|
|
3637
1959
|
for s in rep["sections"]:
|
|
3638
1960
|
lines.append(f" {s['name']:<8} {s['start']:7.2f} – {s['end']:7.2f} s"
|
|
3639
1961
|
f" energy {s['energy']:.2f} 目标 {s['target_lufs']:.1f} LUFS"
|
|
@@ -3657,17 +1979,10 @@ def main(argv=None) -> int:
|
|
|
3657
1979
|
description="scorekit — 程序化配乐(numpy + sfxkit,无采样、确定性)",
|
|
3658
1980
|
)
|
|
3659
1981
|
ap.add_argument("--selftest", action="store_true", help="跑自测(不联网,逐条 PASS/FAIL)")
|
|
3660
|
-
ap.add_argument("--list-styles", action="store_true", help="列出可用配乐风格后退出")
|
|
3661
1982
|
ap.add_argument("--duration", type=float, default=60.0, help="片长(秒),默认 60")
|
|
3662
|
-
ap.add_argument("--
|
|
3663
|
-
help="配乐风格:" + "/".join(MUSIC_STYLE_IDS) + "(缺省 = legacy,与 v0.4 逐样本一致)")
|
|
3664
|
-
ap.add_argument("--mood", default="steady", help="情绪修饰:" + "/".join(MOOD_IDS))
|
|
3665
|
-
ap.add_argument("--fingerprint", default=None,
|
|
3666
|
-
help="内容指纹(v0.7:参与和声/动机/鼓型变奏;只影响 style 路径,legacy 忽略)")
|
|
3667
|
-
ap.add_argument("--bpm", type=float, default=None,
|
|
3668
|
-
help="BPM(缺省:legacy 120;风格路径取该风格 bpm_hint 的中位)")
|
|
1983
|
+
ap.add_argument("--bpm", type=float, default=None, help="BPM,缺省 120")
|
|
3669
1984
|
ap.add_argument("--key", default=None,
|
|
3670
|
-
help="调性,可选 " + "/".join(KEYS_ALL) + "
|
|
1985
|
+
help="调性,可选 " + "/".join(KEYS_ALL) + "(缺省 Dm)")
|
|
3671
1986
|
ap.add_argument("--seed", type=int, default=7, help="随机种子,默认 7")
|
|
3672
1987
|
ap.add_argument("--energy", type=float, default=0.55, help="无 anchors/sections 时的单段能量,默认 0.55")
|
|
3673
1988
|
ap.add_argument("--anchors", default=None, help="timeline.json 或锚点列表文件([{id,kind,start,end}])")
|
|
@@ -3679,46 +1994,31 @@ def main(argv=None) -> int:
|
|
|
3679
1994
|
|
|
3680
1995
|
if args.selftest:
|
|
3681
1996
|
return selftest()
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
sid = _normalize_style_id(args.style)
|
|
3687
|
-
if sid is None:
|
|
3688
|
-
bpm = float(args.bpm) if args.bpm is not None else 120.0
|
|
3689
|
-
key = str(args.key) if args.key else "Dm"
|
|
3690
|
-
else:
|
|
3691
|
-
hint = MUSIC_STYLES[sid]["bpm_hint"]
|
|
3692
|
-
bpm = float(args.bpm) if args.bpm is not None else float(hint[len(hint) // 2])
|
|
3693
|
-
key = str(args.key) if args.key else str(MUSIC_STYLES[sid]["keys"][0])
|
|
1997
|
+
|
|
1998
|
+
bpm = float(args.bpm) if args.bpm is not None else 120.0
|
|
1999
|
+
key = str(args.key) if args.key else "Dm"
|
|
3694
2000
|
_require(key in KEYS_ALL, f"未知调性 {key!r};可选:{'/'.join(KEYS_ALL)}")
|
|
3695
2001
|
anchors = _parse_anchors_file(args.anchors) if args.anchors else None
|
|
3696
2002
|
sections = _parse_sections_file(args.sections) if args.sections else None
|
|
3697
2003
|
sucks = _parse_suck_list(args.suck)
|
|
3698
2004
|
|
|
3699
2005
|
t0 = time.perf_counter()
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
merged = _merge_sections(anchors, sections, float(args.duration))
|
|
3704
|
-
else:
|
|
3705
|
-
merged = [{"name": "全片", "start": 0.0, "end": float(args.duration),
|
|
3706
|
-
"energy": args.energy}]
|
|
3707
|
-
sc = Score(args.duration, bpm=bpm, key=key, seed=args.seed)
|
|
3708
|
-
for s in merged:
|
|
3709
|
-
sc.section(s["name"], s["start"], s["end"], s["energy"])
|
|
3710
|
-
if anchors:
|
|
3711
|
-
sc._anchors = [{"id": a.get("id", ""), "kind": a.get("kind", ""),
|
|
3712
|
-
"start": float(a["start"]), "end": float(a["end"])} for a in anchors]
|
|
3713
|
-
for t, db, dur in sucks:
|
|
3714
|
-
sc.suck(t, db, dur)
|
|
3715
|
-
wav = sc.render()
|
|
3716
|
-
rep = sc.report()
|
|
2006
|
+
# legacy:与确定性路径完全同一段代码(逐字节兼容是硬契约)
|
|
2007
|
+
if anchors or sections:
|
|
2008
|
+
merged = _merge_sections(anchors, sections, float(args.duration))
|
|
3717
2009
|
else:
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
2010
|
+
merged = [{"name": "全片", "start": 0.0, "end": float(args.duration),
|
|
2011
|
+
"energy": args.energy}]
|
|
2012
|
+
sc = Score(args.duration, bpm=bpm, key=key, seed=args.seed)
|
|
2013
|
+
for s in merged:
|
|
2014
|
+
sc.section(s["name"], s["start"], s["end"], s["energy"])
|
|
2015
|
+
if anchors:
|
|
2016
|
+
sc._anchors = [{"id": a.get("id", ""), "kind": a.get("kind", ""),
|
|
2017
|
+
"start": float(a["start"]), "end": float(a["end"])} for a in anchors]
|
|
2018
|
+
for t, db, dur in sucks:
|
|
2019
|
+
sc.suck(t, db, dur)
|
|
2020
|
+
wav = sc.render()
|
|
2021
|
+
rep = sc.report()
|
|
3722
2022
|
dt = time.perf_counter() - t0
|
|
3723
2023
|
rep["render_s"] = round(dt, 3)
|
|
3724
2024
|
wav_path = Path(args.out) if args.out else None
|