Coverage for src/thunderfish/freqanalysis.py: 14%
86 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-19 20:05 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-19 20:05 +0000
1"""
2Analysis of EOD frequencies.
5## Muscial intervals
7- `musical_intervals`: names and frequency ratios of musical intervals
8- `musical_intervals_short`: short names for musical intervals
11## Frequency analysis
13- `freq_diffs()`: matrix with frequency differences.
14- `freq_ratios()`: matrix with frequency ratios.
15- `freq_intervals()`: matrices with musical intervals and deviations thereof.
18## Visualization
20- `plot_freq_diffs()`: plot matrix with frequency differences.
21- `plot_freq_ratios()`: plot matrix with frequency ratios.
22- `plot_musical_intervals()`: plot matrix with musical intervals.
24"""
26import numpy as np
28from matplotlib import ticker
31musical_intervals = {
32 'unison': (1/1, 1, 1, 0), # 1
33 'minor second': (16/15, 16, 15, 1), # 1.0667
34 'major second': (9/8, 9, 8, 2), # 1.125
35 'minor third': (6/5, 6, 5, 3), # 1.2
36 'major third': (5/4, 5, 4, 4), # 1.25
37 'forth': (4/3, 4, 3, 5), # 1.3333
38 'tritone': (45/32, 45, 32, 6), # 1.4063, half way between forth and fifth: 17/6/2=1.4167, sqrt(2)=1.4142
39 'fifth': (3/2, 3, 2, 7), # 1.5
40 'minor sixth': (8/5, 8, 5, 8), # 1.6
41 'major sixth': (5/3, 5, 3, 9), # 1.6667
42 'subminor seventh': (7/4, 7, 4, 9.5), # 1.75
43 'minor seventh': (9/5, 9, 5, 10), # 1.8
44 'major seventh': (15/8, 15, 8, 11), # 1.875
45 'octave': (2/1, 2, 1, 12) # 2
46}
47"""Name, frequency ratio, nominator, denominator, and index of musical intervals
48"""
50musical_intervals_short = {
51 'unison': 'P1',
52 'minor second': 'm2',
53 'major second': 'M2',
54 'minor third': 'm3',
55 'major third': 'M3',
56 'forth': 'P4',
57 'tritone': 'd5',
58 'fifth': 'P5',
59 'minor sixth': 'm6',
60 'major sixth': 'M6',
61 'subminor seventh': 'd7',
62 'minor seventh': 'm7',
63 'major seventh': 'M7',
64 'octave': 'P8'
65}
66"""Short names for musical intervals
67"""
70def freq_diffs(freqs):
71 """Matrix with frequency differences.
73 Parameters
74 ----------
75 freqs: 1-D array of float
76 List of frequencies.
78 Returns
79 -------
80 deltafs: 2-D array of float
81 Matrix with frequency differences for each pair of frequencies.
82 """
83 deltafs = freqs.reshape(-1, 1) - freqs.reshape(1, -1)
84 return deltafs
87def freq_ratios(freqs):
88 """Matrix with frequency ratios.
90 Parameters
91 ----------
92 freqs: 1-D array of float
93 List of frequencies.
95 Returns
96 -------
97 ratios: 2-D array of float
98 Matrix with frequency ratios for each pair of frequencies.
99 """
100 ratios = freqs.reshape(-1, 1)/freqs.reshape(1, -1)
101 return ratios
104def freq_intervals(freqs):
105 """Matrices with musical intervals and deviations thereof.
107 Parameters
108 ----------
109 freqs: 1-D array of float
110 List of frequencies.
112 Returns
113 -------
114 intervals: 2-D array of int
115 Matrix with index of nearest musical interval.
116 diffs: 2-D array of float
117 Matrix with deviation of freqeuncy ratio to nearest
118 musical interval ratio.
119 diff_fracs: 2-D array of float
120 Matrix with deviation of freqeuncy ratio to nearest
121 musical interval ratio relative to musical interval ratio.
122 """
123 all_intervals = np.array([musical_intervals[k][0]
124 for k in musical_intervals])
125 ratios = freqs.reshape(-1, 1)/freqs.reshape(1, -1)
126 intervals = np.zeros(ratios.shape, dtype=int)
127 diffs = np.zeros(ratios.shape)
128 diff_fracs = np.zeros(ratios.shape)
129 for r in range(ratios.shape[0]):
130 for c in range(ratios.shape[1]):
131 if r != c and 0.5 <= ratios[r, c] < 2.05:
132 if ratios[r, c] >= 0.98:
133 ratio = ratios[r, c]
134 else:
135 ratio = 1/ratios[r, c]
136 intervals[r, c] = np.argmin(np.abs(all_intervals - ratio))
137 diffs[r, c] = ratio - all_intervals[intervals[r, c]]
138 diff_fracs[r, c] = diffs[r, c]/all_intervals[intervals[r, c]]
139 else:
140 intervals[r, c] = -1
141 diffs[r, c] = np.nan
142 diff_fracs[r, c] = np.nan
143 return intervals, diffs, diff_fracs
146def plot_freq_diffs(ax, freqs, cmap='seismic', fontsize='large'):
147 """Plot matrix with frequency differences.
149 Parameters
150 ----------
151 ax: matplotlib axes
152 Axes used for plotting.
153 freqs: 1-D array of float
154 List of frequencies.
155 cmap: matplotlib color map
156 Color map for coloring frequency differences.
157 fontsize: str or float
158 Font size for labels indicating frequency differences.
159 """
160 ax.set_title('Differences $\\Delta f$')
161 deltafs = freq_diffs(freqs)
162 vmax = np.max(np.abs(deltafs))
163 cma = ax.pcolormesh(deltafs[::-1, :], cmap=cmap,
164 vmin=-vmax, vmax=vmax)
165 for r in range(deltafs.shape[0]):
166 for c in range(deltafs.shape[1]):
167 ax.text(c + 0.5, r + 0.5, f'{deltafs[-1 - r, c]:.1f}',
168 ha='center', va='center',
169 fontsize=fontsize, clip_on=True,
170 bbox=dict(boxstyle='round,pad=0.1', ec='none',
171 fc='white', alpha=0.8))
172 ax.set_aspect('equal')
173 ax.xaxis.set_major_locator(ticker.FixedLocator(np.arange(len(freqs)) + 0.5))
174 ax.xaxis.set_major_formatter(ticker.FixedFormatter([f'{f:.1f}' for f in freqs]))
175 ax.yaxis.set_major_locator(ticker.FixedLocator(np.arange(len(freqs)) + 0.5))
176 ax.yaxis.set_major_formatter(ticker.FixedFormatter([f'{f:.1f}' for f in reversed(freqs)]))
177 ax.set_xlabel('EOD$f_i$ [Hz]')
178 ax.set_ylabel('EOD$f_j$ [Hz]')
179 ax.get_figure().colorbar(cma, ax=ax, label='$\\Delta f$ [Hz]')
182def plot_freq_ratios(ax, freqs, cmap='seismic', fontsize='large'):
183 """Plot matrix with frequency ratios.
185 Parameters
186 ----------
187 ax: matplotlib axes
188 Axes used for plotting.
189 freqs: 1-D array of float
190 List of frequencies.
191 cmap: matplotlib color map
192 Color map for coloring frequency ratios.
193 fontsize: str or float
194 Font size for labels indicating frequency ratios.
195 """
196 ax.set_title('Ratios $f_1/f_0$')
197 ratios = freq_ratios(freqs)
198 cma = ax.pcolormesh(ratios[::-1, :], cmap=cmap, norm='log',
199 vmin=1/5, vmax=5)
200 for r in range(ratios.shape[0]):
201 for c in range(ratios.shape[1]):
202 ax.text(c + 0.5, r + 0.5, f'{ratios[-1 - r, c]:.3f}',
203 ha='center', va='center',
204 fontsize=fontsize, clip_on=True,
205 bbox=dict(boxstyle='round,pad=0.1', ec='none',
206 fc='white', alpha=0.8))
207 ax.set_aspect('equal')
208 ax.xaxis.set_major_locator(ticker.FixedLocator(np.arange(len(freqs)) + 0.5))
209 ax.xaxis.set_major_formatter(ticker.FixedFormatter([f'{f:.1f}' for f in freqs]))
210 ax.yaxis.set_major_locator(ticker.FixedLocator(np.arange(len(freqs)) + 0.5))
211 ax.yaxis.set_major_formatter(ticker.FixedFormatter([f'{f:.1f}' for f in reversed(freqs)]))
212 ax.set_xlabel('EOD$f_i$ [Hz]')
213 ax.set_ylabel('EOD$f_j$ [Hz]')
214 ax.get_figure().colorbar(cma, ax=ax, label='Ratio', ticks=[1/5, 1/2, 1, 2, 5], format='%g')
217def plot_musical_intervals(ax, freqs, cmap='YlOrRd_r', fontsize='large'):
218 """Plot matrix with musical intervals.
220 Parameters
221 ----------
222 ax: matplotlib axes
223 Axes used for plotting.
224 freqs: 1-D array of float
225 List of frequencies.
226 cmap: matplotlib color map
227 Color map for coloring deviations from musical intervals.
228 fontsize: str or float
229 Font size for labels indicating musical intervals and
230 deviations thereof.
231 """
232 ax.set_title('Musical intervals')
233 all_intervals = np.array([musical_intervals[k][0] for k in musical_intervals])
234 if len(freqs) < 6:
235 all_names = list(musical_intervals.keys())
236 else:
237 all_names = [musical_intervals_short[k] for k in musical_intervals]
238 intervals, diffs, diff_fracs = freq_intervals(freqs)
239 cma = ax.pcolormesh(100*np.abs(diff_fracs[::-1, :]), cmap=cmap,
240 vmin=0, vmax=1)
241 for r in range(intervals.shape[0]):
242 for c in range(intervals.shape[1]):
243 if -1 - r != c and intervals[-1 - r, c] >= 0:
244 idx = intervals[-1 - r, c]
245 if len(freqs) < 6:
246 label = f'{all_intervals[idx]:.4f}\n{all_names[idx]}\n$\\Delta$={diffs[-1 - r, c]:.4f}\n{100*diff_fracs[-1 - r, c]:.1f}%'
247 else:
248 label = f'{all_names[idx]}\n{100*diff_fracs[-1 - r, c]:.1f}%'
249 ax.text(c + 0.5, r + 0.5, label,
250 ha='center', va='center',
251 fontsize=fontsize, clip_on=True,
252 bbox=dict(boxstyle='round,pad=0.1', ec='none',
253 fc='white', alpha=0.8))
254 ax.set_aspect('equal')
255 ax.xaxis.set_major_locator(ticker.FixedLocator(np.arange(len(freqs)) + 0.5))
256 ax.xaxis.set_major_formatter(ticker.FixedFormatter([f'{f:.1f}' for f in freqs]))
257 ax.yaxis.set_major_locator(ticker.FixedLocator(np.arange(len(freqs)) + 0.5))
258 ax.yaxis.set_major_formatter(ticker.FixedFormatter([f'{f:.1f}' for f in reversed(freqs)]))
259 ax.set_xlabel('EOD$f_i$ [Hz]')
260 ax.set_ylabel('EOD$f_j$ [Hz]')
261 ax.get_figure().colorbar(cma, ax=ax, extend='max',
262 format=ticker.PercentFormatter(decimals=1),
263 label='Deviation from musical interval')