Skip to content

Commit f0e1d82

Browse files
committed
Model fitting script.
1 parent c1118c3 commit f0e1d82

File tree

4 files changed

+431
-0
lines changed

4 files changed

+431
-0
lines changed

Model/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
__pycache__
2+
*.txt
3+
*.json

Model/Partials.py

Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
"""
2+
Parse the par-text-frame-format exported from SPEAR.
3+
"""
4+
5+
import numpy as np
6+
from math import log10
7+
import peakutils
8+
from scipy.optimize import curve_fit
9+
10+
#===============================================================================
11+
12+
def attgain(n, p):
13+
att = []
14+
15+
w = 0.05
16+
y = 0.6
17+
18+
if p > 0.0:
19+
y += 0.11 * p
20+
21+
z = 0.0
22+
23+
i = 1
24+
j = 0
25+
26+
while i <= 24:
27+
k = n * i // 24
28+
x = 1.0 - z - 1.5 * y
29+
y += w * x
30+
31+
if k == j:
32+
d = 0.0
33+
else:
34+
d = w * y * p / (k - j)
35+
36+
while j < k:
37+
m = float(j) / float(n)
38+
j += 1
39+
att.append((1.0 - m) * z + m)
40+
z += d
41+
42+
i += 1
43+
44+
return att
45+
46+
def get_attgain(n, level):
47+
def f(x, p):
48+
assert len(x) == n
49+
a = attgain(n, p)
50+
for i in range(n):
51+
a[i] *= level
52+
53+
return a
54+
55+
return f
56+
57+
def median(input):
58+
arr = sorted(input)
59+
return arr[len(arr)//2] if len(arr) % 2 else (arr[len(arr)//2] + arr[len(arr)//2-1])/2
60+
61+
#===============================================================================
62+
63+
class Partial:
64+
def __init__(self):
65+
self.freq = []
66+
self.level = []
67+
self.lowest_freq = 0.0
68+
self.highest_freq = 0.0
69+
self.mid_freq = 0.0
70+
71+
self.mid_level = 0.0
72+
self.level_randomization = 0.0
73+
self.attack = 0
74+
self.attack_profile = 0.0
75+
76+
def calc_freq_range(self):
77+
f_sum = 0.0
78+
f_num = 0
79+
80+
for freq in self.freq:
81+
if freq > 0.0:
82+
f_sum += freq
83+
f_num += 1
84+
85+
if self.lowest_freq == 0.0 or freq < self.lowest_freq:
86+
self.lowest_freq = freq
87+
88+
if self.highest_freq == 0.0 or freq > self.highest_freq:
89+
self.highest_freq = freq
90+
91+
self.mid_freq = 0.0 if f_num == 0 else f_sum/f_num
92+
93+
def characterize(self, time):
94+
levels = np.array(self.level)
95+
peaks = peakutils.indexes(levels, thres=0.01, min_dist=5)
96+
troughs = peakutils.indexes(-levels, thres=0.01, min_dist=5)
97+
98+
self.mid_level = median(self.level)
99+
lev_max = self.mid_level
100+
101+
for peak in peaks:
102+
lev_max = max(lev_max, self.level[peak])
103+
104+
lev_min = lev_max
105+
106+
for trough in troughs:
107+
if self.level[trough] > 0.5*lev_max:
108+
lev_min = min(lev_min, self.level[trough])
109+
110+
self.level_randomization = 20.0 * log10(lev_max / lev_min) if lev_min > 0.0 else 0.0
111+
112+
idx_attack = max(5, peaks[0])
113+
lev_attack = self.level[idx_attack]
114+
115+
while self.level[idx_attack] > 0.5*(lev_attack+self.mid_level):
116+
idx_attack += 1
117+
118+
self.attack = time[idx_attack]
119+
120+
xdata = np.linspace(0, 1, idx_attack)
121+
ydata = self.level[0:idx_attack]
122+
assert len(xdata) == len(ydata)
123+
func = get_attgain(len(xdata), self.mid_level)
124+
125+
popt, pcov = curve_fit(func, xdata, ydata, p0=[2.0], bounds=(-1.4, 10.0))
126+
self.attack_profile = popt[0]
127+
128+
129+
#===============================================================================
130+
131+
class Partials:
132+
def __init__(self):
133+
self._partials_count = 0
134+
self._frame_count = 0
135+
self._time = []
136+
self._partials = []
137+
138+
def read_from_file(self, filePath):
139+
with open(filePath, 'r') as file:
140+
self._read_header(file)
141+
self._read_frame_data(file)
142+
143+
@property
144+
def partials_count(self):
145+
return self._partials_count
146+
147+
@property
148+
def time(self):
149+
return self._time
150+
151+
@property
152+
def partials(self):
153+
return self._partials
154+
155+
def _read_header(self, file):
156+
format = file.readline().rstrip()
157+
158+
if format != "par-text-frame-format":
159+
raise Exception(f"Unexpected file format: {format}")
160+
161+
point_type = file.readline().rstrip()
162+
163+
if point_type != "point-type index frequency amplitude":
164+
raise Exception("Unexpected point type")
165+
166+
partials_count = file.readline().rstrip().split()
167+
168+
if len(partials_count) != 2 or partials_count[0] != "partials-count":
169+
raise Exception("Failed to read the number of partials")
170+
171+
partials_count_num = int(partials_count[1])
172+
173+
if partials_count_num <= 0:
174+
raise Exception(f"Invalid number of partials: {partials_count_num}")
175+
176+
frame_count = file.readline().rstrip().split()
177+
178+
if len(frame_count) != 2 or frame_count[0] != "frame-count":
179+
raise Exception("Failed to read the nmber of frames")
180+
181+
frame_count_num = int(frame_count[1])
182+
183+
if frame_count_num <= 0:
184+
raise Exception(f"Invalid number of frames {frame_count_num}")
185+
186+
frame_data = file.readline().rstrip()
187+
188+
if frame_data != "frame-data":
189+
raise Exception("Frame data not found")
190+
191+
self._partials_count = partials_count_num
192+
self._frame_count = frame_count_num
193+
194+
def _read_frame_data(self, file):
195+
self._time = []
196+
self._partials = []
197+
198+
for i in range(self._partials_count):
199+
self._partials.append(Partial())
200+
201+
eof = False
202+
203+
while not eof:
204+
line = file.readline().rstrip()
205+
206+
if line == "":
207+
eof = True
208+
else:
209+
self._parse_frame_line(line)
210+
211+
for partial in self._partials:
212+
partial.calc_freq_range()
213+
partial.characterize(self._time)
214+
215+
self._partials.sort(key=lambda p: p.mid_freq)
216+
217+
def _parse_frame_line(self, line):
218+
s = line.split()
219+
time = float(s[0])
220+
n_partials = int(s[1])
221+
assert n_partials <= self._partials_count
222+
223+
self._time.append(time)
224+
225+
idx = 2
226+
227+
freq = {}
228+
amp = {}
229+
230+
while idx < len(s):
231+
assert idx + 2 < len(s)
232+
partial_number = int(s[idx])
233+
assert partial_number >= 0 and partial_number < self._partials_count
234+
partial_freq = float(s[idx + 1])
235+
partial_amp = float(s[idx + 2])
236+
237+
freq[partial_number] = partial_freq
238+
amp[partial_number] = partial_amp
239+
240+
idx += 3
241+
242+
for i in range(self._partials_count):
243+
if i in freq:
244+
assert i in amp
245+
self._partials[i].freq.append(freq[i])
246+
self._partials[i].level.append(amp[i])
247+
else:
248+
self._partials[i].freq.append(0.0)
249+
self._partials[i].level.append(0.0)

Model/model_fit.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import json
2+
from math import log10
3+
from Partials import *
4+
5+
partials_files = [
6+
"036-C.txt",
7+
"042-F#.txt",
8+
"048-C.txt",
9+
"054-F#.txt",
10+
"060-C.txt",
11+
"066-F#.txt",
12+
"072-C.txt",
13+
"078-F#.txt",
14+
"084-C.txt",
15+
"090-F#.txt",
16+
"096-C.txt"
17+
]
18+
19+
NUM_HARMONICS = 64
20+
NUM_NOTES = 11
21+
22+
h_lev = []
23+
h_ran = []
24+
h_att = []
25+
h_atp = []
26+
27+
partials_per_note = []
28+
29+
for file in partials_files:
30+
partials = Partials()
31+
partials.read_from_file(file)
32+
partials_per_note.append(partials)
33+
34+
for h in range(NUM_HARMONICS):
35+
lev = [];
36+
ran = [];
37+
att = [];
38+
atp = [];
39+
40+
mask = 0
41+
bit = 1
42+
43+
for pidx in range(NUM_NOTES):
44+
x_lev = -100.0
45+
x_ran = 0.0
46+
x_att = 0.05
47+
x_atp = 0.0
48+
49+
if pidx < len(partials_per_note):
50+
p = partials_per_note[pidx]
51+
52+
if h < len(p.partials):
53+
if p.partials[h].mid_level > 0.0:
54+
mask = mask | bit
55+
x_lev = 20.0 * log10(p.partials[h].mid_level)
56+
57+
x_ran = p.partials[h].level_randomization
58+
x_att = p.partials[h].attack
59+
60+
if x_att > 0.5:
61+
x_att = 0.5
62+
63+
x_atp = p.partials[h].attack_profile if x_att > 0.05 else 0.0
64+
65+
if x_atp > 2.4:
66+
x_atp = 2.4
67+
if x_atp < -1.2:
68+
x_atp = -1.2
69+
70+
lev.append(x_lev)
71+
ran.append(x_ran)
72+
att.append(x_att)
73+
atp.append(x_atp)
74+
75+
bit <<= 1
76+
77+
h_lev.append({
78+
"mask": mask,
79+
"values": lev
80+
})
81+
82+
h_ran.append({
83+
"mask": mask,
84+
"values": ran
85+
})
86+
87+
h_att.append({
88+
"mask": mask,
89+
"values": att
90+
})
91+
92+
h_atp.append({
93+
"mask": mask,
94+
"values": atp
95+
})
96+
97+
output = {
98+
"h_lev": h_lev,
99+
"h_ran": h_ran,
100+
"h_att": h_att,
101+
"h_atp": h_atp
102+
}
103+
104+
result = json.dumps(output, indent=2)
105+
106+
with open("result.json", "w") as f:
107+
f.write(result)
108+

0 commit comments

Comments
 (0)