Nokia123543 commited on
Commit
1295d4a
·
verified ·
1 Parent(s): a919c3e

PLUS: мультипроход со слиянием и проверкой нот по аудио

Browse files
Files changed (1) hide show
  1. plus/verify_notes.py +89 -0
plus/verify_notes.py ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Проверка кандидатов по аудио: есть ли в записи энергия на этой высоте.
2
+
3
+ Модель с узким conditioning начинает выдумывать партии (на тесте 400 нот баса
4
+ на миксе, где бас есть, но не в таких высотах). Поэтому каждый кандидат
5
+ проверяется по звуку: в окне от атаки считается энергия на основной частоте
6
+ (Гёрцель) и сравнивается с медианной энергией по соседним полутонам. Нота
7
+ выживает, если её тон реально выделяется.
8
+ """
9
+
10
+ import math
11
+ import wave
12
+
13
+ import numpy
14
+
15
+
16
+ def load_wav(path):
17
+ with wave.open(path, "rb") as handle:
18
+ rate = handle.getframerate()
19
+ channels = handle.getnchannels()
20
+ width = handle.getsampwidth()
21
+ frames = handle.readframes(handle.getnframes())
22
+ if width != 2:
23
+ raise ValueError("поддерживается 16-битный PCM")
24
+ data = numpy.frombuffer(frames, dtype="<i2").astype("float32") / 32768.0
25
+ if channels > 1:
26
+ data = data.reshape(-1, channels).mean(axis=1)
27
+ return data, rate
28
+
29
+
30
+ def goertzel(samples, rate, freq):
31
+ """Энергия на одной частоте без полного FFT."""
32
+ n = len(samples)
33
+ if n < 32 or freq <= 0:
34
+ return 0.0
35
+ k = int(0.5 + n * freq / rate)
36
+ omega = 2.0 * math.pi * k / n
37
+ coeff = 2.0 * math.cos(omega)
38
+ s_prev = s_prev2 = 0.0
39
+ for value in samples:
40
+ s = value + coeff * s_prev - s_prev2
41
+ s_prev2, s_prev = s_prev, s
42
+ power = s_prev2 ** 2 + s_prev ** 2 - coeff * s_prev * s_prev2
43
+ return max(0.0, power) / (n * n)
44
+
45
+
46
+ def pitch_energy(samples, rate, midi):
47
+ freq = 440.0 * 2 ** ((midi - 69) / 12.0)
48
+ return goertzel(samples, rate, freq) + goertzel(samples, rate, freq * 2)
49
+
50
+
51
+ def verify(notes, audio_path, window=0.25, neighbours=3, threshold=2.0):
52
+ """Отметить ноты как подтверждённые записью. Возвращает (kept, dropped, stats)."""
53
+ samples, rate = load_wav(audio_path)
54
+ length = max(64, int(window * rate))
55
+ kept, dropped = [], []
56
+ for note in notes:
57
+ if note.get("instrument") == "drums" or note.get("is_drum"):
58
+ note["energy_ratio"] = None # у барабанов нет основного тона
59
+ kept.append(note)
60
+ continue
61
+ start = int(note["start"] * rate)
62
+ if start >= len(samples):
63
+ dropped.append(note)
64
+ continue
65
+ chunk = samples[start:start + length]
66
+ if len(chunk) < 64:
67
+ dropped.append(note)
68
+ continue
69
+ mine = pitch_energy(chunk, rate, note["pitch"])
70
+ reference = []
71
+ for step in range(1, neighbours + 1):
72
+ for direction in (-1, 1):
73
+ reference.append(pitch_energy(chunk, rate, note["pitch"] + direction * step))
74
+ floor = float(numpy.median(reference)) + 1e-9
75
+ ratio = mine / floor
76
+ note["energy_ratio"] = round(ratio, 2)
77
+ if ratio >= threshold or note.get("support", 1) >= 2:
78
+ kept.append(note)
79
+ else:
80
+ dropped.append(note)
81
+ stats = {
82
+ "kept": len(kept),
83
+ "dropped": len(dropped),
84
+ "threshold": threshold,
85
+ }
86
+ pitched = [n["energy_ratio"] for n in kept if n.get("energy_ratio") is not None]
87
+ if pitched:
88
+ stats["median_ratio"] = round(float(numpy.median(pitched)), 2)
89
+ return kept, dropped, stats