|
| 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) |
0 commit comments