Skip to content

Commit f36ef8f

Browse files
committed
NEW
1 parent f7031f6 commit f36ef8f

25 files changed

Lines changed: 453 additions & 0 deletions

src/pkg/Fymew/compose.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,7 @@
1010
预制轮播
1111

1212

13+
# fymew_guide
14+
15+
案例图片
16+

src/pkg/Fymew/fymew_guide/Fymew1.0.0.md

Whitespace-only changes.
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import numpy as np
2+
import librosa
3+
import librosa.display
4+
import matplotlib.pyplot as plt
5+
from sklearn.cluster import KMeans
6+
from sklearn.metrics.pairwise import euclidean_distances
7+
import json
8+
import os
9+
10+
11+
def detect_chorus_auto(audio_path, sr=22050, n_bins=72, hop_length=512, m=None, visualize=True):
12+
"""自动自适应谱聚类副歌检测"""
13+
print(f"🎧 加载音频中:{audio_path}")
14+
y, sr = librosa.load(audio_path, sr=sr, mono=True)
15+
16+
# 🥁 Beat tracking
17+
tempo, beats = librosa.beat.beat_track(y=y, sr=sr, hop_length=hop_length)
18+
tempo = float(np.atleast_1d(tempo)[0])
19+
beat_times = librosa.frames_to_time(beats, sr=sr, hop_length=hop_length)
20+
n_beats = len(beats)
21+
print(f"✅ 检测到 {n_beats} 个节拍, tempo={tempo:.1f} BPM")
22+
23+
if n_beats < 8:
24+
raise ValueError("节拍太少,无法分析,请尝试另一首歌曲。")
25+
26+
# 自动选择谱聚类维度
27+
if m is None:
28+
m = min(6, max(3, n_beats // 100))
29+
print(f"📊 自动选择聚类层次数 m={m}")
30+
31+
# 🎼 特征提取
32+
C = np.abs(librosa.cqt(y, sr=sr, hop_length=hop_length, n_bins=n_bins))
33+
C_sync = librosa.util.sync(C, beats, aggregate=np.mean).T
34+
35+
M = librosa.feature.mfcc(y=y, sr=sr, hop_length=hop_length, n_mfcc=13)
36+
M_sync = librosa.util.sync(M, beats, aggregate=np.mean).T
37+
38+
rms = librosa.feature.rms(y=y, frame_length=2048, hop_length=hop_length)[0]
39+
rms_sync = librosa.util.sync(rms, beats, aggregate=np.mean)
40+
41+
# 📈 高斯相似度
42+
def gaussian_affinity(X, k=8):
43+
dist = euclidean_distances(X, X)
44+
sigma = np.mean(np.sort(dist, axis=1)[:, k])
45+
return np.exp(-dist**2 / (2 * sigma**2 + 1e-8))
46+
47+
S_rep = gaussian_affinity(C_sync, k=8)
48+
S_loc = gaussian_affinity(M_sync, k=8)
49+
n = S_rep.shape[0]
50+
51+
# 🔁 Recurrence matrix (Eq.1)
52+
k = 8
53+
knn = np.argsort(euclidean_distances(C_sync, C_sync), axis=1)[:, 1:k+1]
54+
R = np.zeros((n, n))
55+
for i in range(n):
56+
for j in knn[i]:
57+
if i in knn[j]:
58+
R[i, j] = 1
59+
R = np.maximum(R, R.T)
60+
61+
# 平滑
62+
w = 3
63+
R_prime = np.zeros_like(R)
64+
for i in range(n):
65+
for j in range(n):
66+
votes = [R[i+t, j+t] for t in range(-w, w+1)
67+
if 0 <= i+t < n and 0 <= j+t < n]
68+
R_prime[i, j] = np.round(np.mean(votes))
69+
70+
# 序列连接 + 加权
71+
Delta = np.eye(n, k=1) + np.eye(n, k=-1)
72+
dR = np.sum(R_prime, axis=1)
73+
dD = np.sum(Delta, axis=1)
74+
mu = np.dot(dD, dR + dD) / (np.sum((dR + dD)**2) + 1e-8)
75+
A = mu * (R_prime * S_rep) + (1 - mu) * (Delta * S_loc)
76+
77+
# 拉普拉斯矩阵
78+
D = np.diag(np.sum(A, axis=1))
79+
D_inv_sqrt = np.diag(1.0 / np.sqrt(np.sum(A, axis=1) + 1e-8))
80+
L = np.eye(n) - D_inv_sqrt @ A @ D_inv_sqrt
81+
L += np.eye(n) * 1e-6
82+
83+
# 谱分解
84+
eigvals, eigvecs = np.linalg.eigh(L)
85+
eigvecs = eigvecs[:, :m]
86+
Y = eigvecs / (np.linalg.norm(eigvecs, axis=1, keepdims=True) + 1e-8)
87+
labels = KMeans(n_clusters=m, n_init=10, random_state=42).fit_predict(Y)
88+
89+
# 找边界
90+
boundaries = np.where(np.diff(labels) != 0)[0]
91+
boundary_times = beat_times[boundaries]
92+
93+
# 🔎 计算每段得分(重复度 + 能量)
94+
scores = []
95+
for k_label in set(labels):
96+
idx = np.where(labels == k_label)[0]
97+
submat = R_prime[np.ix_(idx, idx)]
98+
repeat_score = np.sum(submat)
99+
energy_score = np.mean(rms_sync[idx])
100+
scores.append(repeat_score * 0.7 + energy_score * 0.3)
101+
102+
chorus_label = np.argmax(scores)
103+
chorus_start_idx = np.where(labels == chorus_label)[0][0]
104+
chorus_start_time = beat_times[chorus_start_idx]
105+
106+
# 🧠 自动修正:避免前奏误判
107+
if chorus_start_time < 15:
108+
mid = len(beat_times)//3
109+
chorus_start_time = float(np.median(beat_times[mid:mid*2]))
110+
print(f"⚠️ 副歌过早,自动修正到中段 {chorus_start_time:.2f} 秒")
111+
112+
print(f"🎵 最终副歌起点 ≈ {chorus_start_time:.2f} 秒")
113+
114+
# 保存结果
115+
out_path = os.path.splitext(audio_path)[0] + "_chorus.json"
116+
data = {
117+
"file": os.path.basename(audio_path),
118+
"chorus_start_sec": float(chorus_start_time),
119+
"tempo": float(tempo),
120+
"segments": int(m)
121+
}
122+
with open(out_path, "w", encoding="utf-8") as f:
123+
json.dump(data, f, ensure_ascii=False, indent=2)
124+
print(f"💾 结果已保存到: {out_path}")
125+
126+
# 可视化
127+
if visualize:
128+
fig, ax = plt.subplots(1, 3, figsize=(15, 4))
129+
librosa.display.specshow(R_prime, x_axis='time', y_axis='time', ax=ax[0])
130+
ax[0].set_title("Recurrence matrix R′")
131+
132+
ax[1].scatter(range(len(labels)), labels, c=labels, cmap='tab10', s=20)
133+
ax[1].set_title("Spectral clustering structure")
134+
ax[1].set_xlabel("Beat index")
135+
ax[1].set_ylabel("Segment label")
136+
137+
min_len = min(len(beat_times), len(rms_sync))
138+
ax[2].plot(beat_times[:min_len], rms_sync[:min_len], color='gray')
139+
ax[2].axvline(chorus_start_time, color='r', linestyle='--', label='Chorus start')
140+
ax[2].set_title("RMS Energy vs Time")
141+
ax[2].set_xlabel("Time (s)")
142+
ax[2].legend()
143+
144+
plt.tight_layout()
145+
plt.show()
146+
147+
return chorus_start_time, boundary_times, labels
148+
149+
150+
# 🧪 示例调用
151+
if __name__ == "__main__":
152+
chorus_time, boundaries, labels = detect_chorus_auto(
153+
r"C:\Users\20281\Desktop\music\李荣浩 - 年少有为.mp3",
154+
visualize=True
155+
)
156+
print(f"\n🎯 副歌起点 ≈ {chorus_time:.2f} 秒")

0 commit comments

Comments
 (0)