boris-behav-obs 9.7.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- boris/__init__.py +26 -0
- boris/__main__.py +25 -0
- boris/about.py +143 -0
- boris/add_modifier.py +635 -0
- boris/add_modifier_ui.py +303 -0
- boris/advanced_event_filtering.py +455 -0
- boris/analysis_plugins/__init__.py +0 -0
- boris/analysis_plugins/_latency.py +59 -0
- boris/analysis_plugins/irr_cohen_kappa.py +109 -0
- boris/analysis_plugins/irr_cohen_kappa_with_modifiers.py +112 -0
- boris/analysis_plugins/irr_weighted_cohen_kappa.py +157 -0
- boris/analysis_plugins/irr_weighted_cohen_kappa_with_modifiers.py +162 -0
- boris/analysis_plugins/list_of_dataframe_columns.py +22 -0
- boris/analysis_plugins/number_of_occurences.py +22 -0
- boris/analysis_plugins/number_of_occurences_by_independent_variable.py +54 -0
- boris/analysis_plugins/time_budget.py +61 -0
- boris/behav_coding_map_creator.py +1110 -0
- boris/behavior_binary_table.py +305 -0
- boris/behaviors_coding_map.py +239 -0
- boris/boris_cli.py +340 -0
- boris/cmd_arguments.py +49 -0
- boris/coding_pad.py +280 -0
- boris/config.py +785 -0
- boris/config_file.py +356 -0
- boris/connections.py +409 -0
- boris/converters.py +333 -0
- boris/converters_ui.py +225 -0
- boris/cooccurence.py +250 -0
- boris/core.py +5901 -0
- boris/core_qrc.py +15958 -0
- boris/core_ui.py +1107 -0
- boris/db_functions.py +324 -0
- boris/dev.py +134 -0
- boris/dialog.py +1108 -0
- boris/duration_widget.py +238 -0
- boris/edit_event.py +245 -0
- boris/edit_event_ui.py +233 -0
- boris/event_operations.py +1040 -0
- boris/events_cursor.py +61 -0
- boris/events_snapshots.py +596 -0
- boris/exclusion_matrix.py +141 -0
- boris/export_events.py +1006 -0
- boris/export_observation.py +1203 -0
- boris/external_processes.py +332 -0
- boris/geometric_measurement.py +941 -0
- boris/gui_utilities.py +135 -0
- boris/image_overlay.py +72 -0
- boris/import_observations.py +242 -0
- boris/ipc_mpv.py +325 -0
- boris/irr.py +634 -0
- boris/latency.py +244 -0
- boris/measurement_widget.py +161 -0
- boris/media_file.py +115 -0
- boris/menu_options.py +213 -0
- boris/modifier_coding_map_creator.py +1013 -0
- boris/modifiers_coding_map.py +157 -0
- boris/mpv.py +2016 -0
- boris/mpv2.py +2193 -0
- boris/observation.py +1453 -0
- boris/observation_operations.py +2538 -0
- boris/observation_ui.py +679 -0
- boris/observations_list.py +337 -0
- boris/otx_parser.py +442 -0
- boris/param_panel.py +201 -0
- boris/param_panel_ui.py +305 -0
- boris/player_dock_widget.py +198 -0
- boris/plot_data_module.py +536 -0
- boris/plot_events.py +634 -0
- boris/plot_events_rt.py +237 -0
- boris/plot_spectrogram_rt.py +316 -0
- boris/plot_waveform_rt.py +230 -0
- boris/plugins.py +431 -0
- boris/portion/__init__.py +31 -0
- boris/portion/const.py +95 -0
- boris/portion/dict.py +365 -0
- boris/portion/func.py +52 -0
- boris/portion/interval.py +581 -0
- boris/portion/io.py +181 -0
- boris/preferences.py +510 -0
- boris/preferences_ui.py +770 -0
- boris/project.py +2007 -0
- boris/project_functions.py +2041 -0
- boris/project_import_export.py +1096 -0
- boris/project_ui.py +794 -0
- boris/qrc_boris.py +10389 -0
- boris/qrc_boris5.py +2579 -0
- boris/select_modifiers.py +312 -0
- boris/select_observations.py +210 -0
- boris/select_subj_behav.py +286 -0
- boris/state_events.py +197 -0
- boris/subjects_pad.py +106 -0
- boris/synthetic_time_budget.py +290 -0
- boris/time_budget_functions.py +1136 -0
- boris/time_budget_widget.py +1039 -0
- boris/transitions.py +365 -0
- boris/utilities.py +1810 -0
- boris/version.py +24 -0
- boris/video_equalizer.py +159 -0
- boris/video_equalizer_ui.py +248 -0
- boris/video_operations.py +310 -0
- boris/view_df.py +104 -0
- boris/view_df_ui.py +75 -0
- boris/write_event.py +538 -0
- boris_behav_obs-9.7.7.dist-info/METADATA +139 -0
- boris_behav_obs-9.7.7.dist-info/RECORD +109 -0
- boris_behav_obs-9.7.7.dist-info/WHEEL +5 -0
- boris_behav_obs-9.7.7.dist-info/entry_points.txt +2 -0
- boris_behav_obs-9.7.7.dist-info/licenses/LICENSE.TXT +674 -0
- boris_behav_obs-9.7.7.dist-info/top_level.txt +1 -0
boris/plot_events_rt.py
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BORIS
|
|
3
|
+
Behavioral Observation Research Interactive Software
|
|
4
|
+
Copyright 2012-2025 Olivier Friard
|
|
5
|
+
|
|
6
|
+
This file is part of BORIS.
|
|
7
|
+
|
|
8
|
+
BORIS is free software; you can redistribute it and/or modify
|
|
9
|
+
it under the terms of the GNU General Public License as published by
|
|
10
|
+
the Free Software Foundation; either version 3 of the License, or
|
|
11
|
+
any later version.
|
|
12
|
+
|
|
13
|
+
BORIS is distributed in the hope that it will be useful,
|
|
14
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
15
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
16
|
+
GNU General Public License for more details.
|
|
17
|
+
|
|
18
|
+
You should have received a copy of the GNU General Public License
|
|
19
|
+
along with this program; if not see <http://www.gnu.org/licenses/>.
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
Plot events in real time
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
import matplotlib
|
|
27
|
+
|
|
28
|
+
matplotlib.use("QtAgg")
|
|
29
|
+
|
|
30
|
+
import numpy as np
|
|
31
|
+
from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel
|
|
32
|
+
from PySide6.QtCore import Signal, QEvent, Qt
|
|
33
|
+
from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas
|
|
34
|
+
|
|
35
|
+
from matplotlib.figure import Figure
|
|
36
|
+
|
|
37
|
+
from . import config as cfg
|
|
38
|
+
|
|
39
|
+
# matplotlib.pyplot.switch_backend("Qt5Agg")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class Plot_events_RT(QWidget):
|
|
43
|
+
# send keypress event to mainwindow
|
|
44
|
+
sendEvent = Signal(QEvent)
|
|
45
|
+
|
|
46
|
+
def __init__(self):
|
|
47
|
+
super().__init__()
|
|
48
|
+
self.setWindowTitle("Events plot")
|
|
49
|
+
|
|
50
|
+
self.interval = 60 # default interval of visualization (in seconds)
|
|
51
|
+
self.time_mem = -1
|
|
52
|
+
|
|
53
|
+
self.events_mem = {"init": 0}
|
|
54
|
+
|
|
55
|
+
self.cursor_color = cfg.REALTIME_PLOT_CURSOR_COLOR # default cursor color
|
|
56
|
+
self.observation_type = cfg.MEDIA
|
|
57
|
+
self.groupby = "behaviors" # group results by "behaviors" or "modifiers"
|
|
58
|
+
|
|
59
|
+
self.figure = Figure()
|
|
60
|
+
self.ax = self.figure.add_subplot(1, 1, 1)
|
|
61
|
+
|
|
62
|
+
self.canvas = FigureCanvas(self.figure)
|
|
63
|
+
|
|
64
|
+
layout = QVBoxLayout()
|
|
65
|
+
layout.addWidget(self.canvas)
|
|
66
|
+
|
|
67
|
+
hlayout1 = QHBoxLayout()
|
|
68
|
+
hlayout1.addWidget(QLabel("Time interval"))
|
|
69
|
+
hlayout1.addWidget(QPushButton("+", self, clicked=lambda: self.time_interval_changed(1), focusPolicy=Qt.NoFocus))
|
|
70
|
+
hlayout1.addWidget(QPushButton("-", self, clicked=lambda: self.time_interval_changed(-1), focusPolicy=Qt.NoFocus))
|
|
71
|
+
self.pb_mode = QPushButton("Include modifiers", self, clicked=self.change_mode, focusPolicy=Qt.NoFocus)
|
|
72
|
+
hlayout1.addWidget(self.pb_mode)
|
|
73
|
+
layout.addLayout(hlayout1)
|
|
74
|
+
|
|
75
|
+
self.setLayout(layout)
|
|
76
|
+
|
|
77
|
+
self.installEventFilter(self)
|
|
78
|
+
|
|
79
|
+
def eventFilter(self, receiver, event):
|
|
80
|
+
"""
|
|
81
|
+
send event (if keypress) to main window
|
|
82
|
+
"""
|
|
83
|
+
if event.type() == QEvent.KeyPress:
|
|
84
|
+
self.sendEvent.emit(event)
|
|
85
|
+
return True
|
|
86
|
+
else:
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
def change_mode(self) -> None:
|
|
90
|
+
"""
|
|
91
|
+
Change plot mode
|
|
92
|
+
"behaviors" -> plot behaviors without modifiers
|
|
93
|
+
"modifiers" -> plot behaviors and modifiers
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
if self.groupby == "behaviors":
|
|
97
|
+
self.groupby = "modifiers"
|
|
98
|
+
self.pb_mode.setText("Show behaviors w/o modifiers")
|
|
99
|
+
else:
|
|
100
|
+
self.groupby = "behaviors"
|
|
101
|
+
self.pb_mode.setText("Include modifiers")
|
|
102
|
+
|
|
103
|
+
def time_interval_changed(self, action: int) -> None:
|
|
104
|
+
"""
|
|
105
|
+
change the time interval for events plot
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
action (int): -1 decrease time interval, +1 increase time interval
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
None
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
if action == -1 and self.interval <= 5:
|
|
115
|
+
return
|
|
116
|
+
self.interval += 5 * action
|
|
117
|
+
self.plot_events(current_time=self.time_mem, force_plot=True)
|
|
118
|
+
|
|
119
|
+
def aggregate_events(self, events: list, start: float, end: float) -> dict:
|
|
120
|
+
"""
|
|
121
|
+
aggregate state events
|
|
122
|
+
take consideration of subject and modifiers
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
events (list): list of events
|
|
126
|
+
start (float): initial value
|
|
127
|
+
end (float): final value
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
dict
|
|
131
|
+
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
def group(subject: str, code: str, modifier: str) -> tuple:
|
|
135
|
+
if self.groupby == "behaviors":
|
|
136
|
+
return (subject, code)
|
|
137
|
+
else: # with modifiers
|
|
138
|
+
return (subject, code, modifier)
|
|
139
|
+
|
|
140
|
+
mem_behav = {}
|
|
141
|
+
intervals_behav = {}
|
|
142
|
+
|
|
143
|
+
for event in events:
|
|
144
|
+
intervals_behav[group(event[1], event[2], event[3])] = [(0, 0)]
|
|
145
|
+
|
|
146
|
+
for event in events:
|
|
147
|
+
time_, subject, code, modifier = event[:4]
|
|
148
|
+
key = group(subject, code, modifier)
|
|
149
|
+
|
|
150
|
+
# check if code is state
|
|
151
|
+
if code in self.state_events_list:
|
|
152
|
+
if key in mem_behav and mem_behav[key] is not None:
|
|
153
|
+
# stop interval
|
|
154
|
+
|
|
155
|
+
# check if event is in interval start-end
|
|
156
|
+
if any(
|
|
157
|
+
(
|
|
158
|
+
start <= mem_behav[key] <= end,
|
|
159
|
+
start <= time_ <= end,
|
|
160
|
+
mem_behav[key] <= start and time_ > end,
|
|
161
|
+
)
|
|
162
|
+
):
|
|
163
|
+
intervals_behav[key].append((float(mem_behav[key]), float(time_)))
|
|
164
|
+
mem_behav[key] = None
|
|
165
|
+
else:
|
|
166
|
+
# start interval
|
|
167
|
+
mem_behav[key] = time_
|
|
168
|
+
|
|
169
|
+
else: # point event
|
|
170
|
+
if start <= time_ <= end:
|
|
171
|
+
intervals_behav[key].append((float(time_), float(time_) + self.point_event_plot_duration * 50)) # point event -> 1 s
|
|
172
|
+
|
|
173
|
+
# check if intervals are closed
|
|
174
|
+
for k in mem_behav:
|
|
175
|
+
if mem_behav[k] is not None: # interval open
|
|
176
|
+
if self.observation_type == cfg.LIVE:
|
|
177
|
+
intervals_behav[k].append((float(mem_behav[k]), float((end + start) / 2))) # close interval with current time
|
|
178
|
+
|
|
179
|
+
elif self.observation_type == cfg.MEDIA:
|
|
180
|
+
intervals_behav[k].append((float(mem_behav[k]), float(end))) # close interval with end value
|
|
181
|
+
|
|
182
|
+
return intervals_behav
|
|
183
|
+
|
|
184
|
+
def plot_events(self, current_time: float, force_plot: bool = False):
|
|
185
|
+
"""
|
|
186
|
+
plot events centered on the current time
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
current_time (float): time for displaying events
|
|
190
|
+
force_plot (bool): force plot even if media paused
|
|
191
|
+
"""
|
|
192
|
+
|
|
193
|
+
self.events = self.aggregate_events(self.events_list, current_time - self.interval / 2, current_time + self.interval / 2)
|
|
194
|
+
|
|
195
|
+
if not force_plot and current_time == self.time_mem:
|
|
196
|
+
return
|
|
197
|
+
|
|
198
|
+
self.time_mem = current_time
|
|
199
|
+
|
|
200
|
+
if self.events != self.events_mem:
|
|
201
|
+
left, duration = {}, {}
|
|
202
|
+
for k in self.events:
|
|
203
|
+
left[k] = []
|
|
204
|
+
duration[k] = []
|
|
205
|
+
for interv in self.events[k]:
|
|
206
|
+
left[k].append(interv[0])
|
|
207
|
+
duration[k].append(interv[1] - interv[0])
|
|
208
|
+
|
|
209
|
+
self.behaviors, self.durations, self.lefts, self.colors = [], [], [], []
|
|
210
|
+
for k in self.events:
|
|
211
|
+
if self.groupby == "behaviors":
|
|
212
|
+
subject_name, bevavior_code = k
|
|
213
|
+
if subject_name == "":
|
|
214
|
+
subject_name = "No focal"
|
|
215
|
+
behav_col = self.behav_color[bevavior_code]
|
|
216
|
+
self.behaviors.extend([f"{subject_name} - {bevavior_code}"] * len(self.events[k]))
|
|
217
|
+
self.colors.extend([behav_col] * len(self.events[k]))
|
|
218
|
+
else: # with modifiers
|
|
219
|
+
subject_name, bevavior_code, modifier = k
|
|
220
|
+
behav_col = self.behav_color[bevavior_code]
|
|
221
|
+
self.behaviors.extend([f"{subject_name} - {bevavior_code} ({modifier})"] * len(self.events[k]))
|
|
222
|
+
self.colors.extend([behav_col] * len(self.events[k]))
|
|
223
|
+
|
|
224
|
+
self.lefts.extend(left[k])
|
|
225
|
+
self.durations.extend(duration[k])
|
|
226
|
+
|
|
227
|
+
self.events_mem = self.events
|
|
228
|
+
|
|
229
|
+
self.ax.clear()
|
|
230
|
+
self.ax.set_xlim(current_time - self.interval / 2, current_time + self.interval / 2)
|
|
231
|
+
|
|
232
|
+
self.ax.axvline(x=current_time, color=self.cursor_color, linestyle="-")
|
|
233
|
+
|
|
234
|
+
self.ax.barh(y=np.array(self.behaviors), width=self.durations, left=self.lefts, color=self.colors, height=0.5)
|
|
235
|
+
|
|
236
|
+
self.canvas.draw()
|
|
237
|
+
self.figure.canvas.flush_events()
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BORIS
|
|
3
|
+
Behavioral Observation Research Interactive Software
|
|
4
|
+
Copyright 2012-2025 Olivier Friard
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
This program is free software; you can redistribute it and/or modify
|
|
8
|
+
it under the terms of the GNU General Public License as published by
|
|
9
|
+
the Free Software Foundation; either version 2 of the License, or
|
|
10
|
+
(at your option) any later version.
|
|
11
|
+
|
|
12
|
+
This program is distributed in the hope that it will be useful,
|
|
13
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
14
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
15
|
+
GNU General Public License for more details.
|
|
16
|
+
|
|
17
|
+
You should have received a copy of the GNU General Public License
|
|
18
|
+
along with this program; if not, write to the Free Software
|
|
19
|
+
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
|
20
|
+
MA 02110-1301, USA.
|
|
21
|
+
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import wave
|
|
25
|
+
import matplotlib
|
|
26
|
+
|
|
27
|
+
matplotlib.use("QtAgg")
|
|
28
|
+
|
|
29
|
+
import numpy as np
|
|
30
|
+
from scipy import signal
|
|
31
|
+
from . import config as cfg
|
|
32
|
+
|
|
33
|
+
from PySide6.QtWidgets import (
|
|
34
|
+
QWidget,
|
|
35
|
+
QVBoxLayout,
|
|
36
|
+
QHBoxLayout,
|
|
37
|
+
QPushButton,
|
|
38
|
+
QLabel,
|
|
39
|
+
QSpinBox,
|
|
40
|
+
)
|
|
41
|
+
from PySide6.QtCore import Signal, QEvent, Qt
|
|
42
|
+
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
|
|
43
|
+
from matplotlib.figure import Figure
|
|
44
|
+
import matplotlib.ticker as mticker
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class Plot_spectrogram_RT(QWidget):
|
|
48
|
+
# send keypress event to mainwindow
|
|
49
|
+
sendEvent = Signal(QEvent)
|
|
50
|
+
|
|
51
|
+
def __init__(self):
|
|
52
|
+
super().__init__()
|
|
53
|
+
self.setWindowTitle("Spectrogram")
|
|
54
|
+
|
|
55
|
+
self.interval = 10 # interval of visualization (in seconds)
|
|
56
|
+
self.time_mem = -1
|
|
57
|
+
|
|
58
|
+
self.cursor_color = cfg.REALTIME_PLOT_CURSOR_COLOR
|
|
59
|
+
|
|
60
|
+
self.spectro_color_map = matplotlib.pyplot.get_cmap("viridis")
|
|
61
|
+
|
|
62
|
+
self.figure = Figure()
|
|
63
|
+
self.ax = self.figure.add_subplot(1, 1, 1)
|
|
64
|
+
|
|
65
|
+
self.canvas = FigureCanvas(self.figure)
|
|
66
|
+
|
|
67
|
+
layout = QVBoxLayout()
|
|
68
|
+
layout.addWidget(self.canvas)
|
|
69
|
+
|
|
70
|
+
hlayout1 = QHBoxLayout()
|
|
71
|
+
hlayout1.addWidget(QLabel("Time interval"))
|
|
72
|
+
hlayout1.addWidget(
|
|
73
|
+
QPushButton(
|
|
74
|
+
"+",
|
|
75
|
+
self,
|
|
76
|
+
clicked=lambda: self.time_interval_changed(1),
|
|
77
|
+
focusPolicy=Qt.NoFocus,
|
|
78
|
+
)
|
|
79
|
+
)
|
|
80
|
+
hlayout1.addWidget(
|
|
81
|
+
QPushButton(
|
|
82
|
+
"-",
|
|
83
|
+
self,
|
|
84
|
+
clicked=lambda: self.time_interval_changed(-1),
|
|
85
|
+
focusPolicy=Qt.NoFocus,
|
|
86
|
+
)
|
|
87
|
+
)
|
|
88
|
+
layout.addLayout(hlayout1)
|
|
89
|
+
|
|
90
|
+
hlayout2 = QHBoxLayout()
|
|
91
|
+
hlayout2.addWidget(QLabel("Frequency interval"))
|
|
92
|
+
self.sb_freq_min = QSpinBox(valueChanged=self.frequency_interval_changed, focusPolicy=Qt.NoFocus)
|
|
93
|
+
self.sb_freq_min.setRange(0, 200000)
|
|
94
|
+
self.sb_freq_min.setSingleStep(100)
|
|
95
|
+
self.sb_freq_max = QSpinBox(valueChanged=self.frequency_interval_changed, focusPolicy=Qt.NoFocus)
|
|
96
|
+
self.sb_freq_max.setRange(0, 200000)
|
|
97
|
+
self.sb_freq_max.setSingleStep(100)
|
|
98
|
+
hlayout2.addWidget(self.sb_freq_min)
|
|
99
|
+
hlayout2.addWidget(self.sb_freq_max)
|
|
100
|
+
layout.addLayout(hlayout2)
|
|
101
|
+
|
|
102
|
+
self.setLayout(layout)
|
|
103
|
+
|
|
104
|
+
self.installEventFilter(self)
|
|
105
|
+
|
|
106
|
+
def eventFilter(self, receiver, event):
|
|
107
|
+
"""
|
|
108
|
+
send event (if keypress) to main window
|
|
109
|
+
"""
|
|
110
|
+
if event.type() == QEvent.KeyPress:
|
|
111
|
+
self.sendEvent.emit(event)
|
|
112
|
+
return True
|
|
113
|
+
else:
|
|
114
|
+
return False
|
|
115
|
+
|
|
116
|
+
def get_wav_info(self, wav_file: str) -> tuple[np.array, int]:
|
|
117
|
+
"""
|
|
118
|
+
read wav file and extract information
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
wav_file (str): path of wav file
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
np.array: signal contained in wav file
|
|
125
|
+
int: frame rate of wav file
|
|
126
|
+
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
wav = wave.open(wav_file, "r")
|
|
131
|
+
frames = wav.readframes(-1)
|
|
132
|
+
# sound_info = np.fromstring(frames, dtype=np.int16)
|
|
133
|
+
sound_info = np.frombuffer(frames, dtype=np.int16)
|
|
134
|
+
frame_rate = wav.getframerate()
|
|
135
|
+
wav.close()
|
|
136
|
+
return sound_info, frame_rate
|
|
137
|
+
except Exception:
|
|
138
|
+
return np.array([]), 0
|
|
139
|
+
|
|
140
|
+
def time_interval_changed(self, action: int):
|
|
141
|
+
"""
|
|
142
|
+
change the time interval for plotting spectrogram
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
action (int): -1 decrease time interval, +1 increase time interval
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
None
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
if action == -1 and self.interval <= 5:
|
|
152
|
+
return
|
|
153
|
+
self.interval += 5 * action
|
|
154
|
+
self.plot_spectro(current_time=self.time_mem, force_plot=True)
|
|
155
|
+
|
|
156
|
+
def frequency_interval_changed(self):
|
|
157
|
+
"""
|
|
158
|
+
change the frequency interval for plotting spectrogram
|
|
159
|
+
"""
|
|
160
|
+
self.plot_spectro(current_time=self.time_mem, force_plot=True)
|
|
161
|
+
|
|
162
|
+
def load_wav(self, wav_file_path: str) -> dict:
|
|
163
|
+
"""
|
|
164
|
+
load wav file in numpy array
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
wav_file_path (str): path of wav file
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
dict: "error" key if error, "media_length" and "frame_rate"
|
|
171
|
+
"""
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
self.sound_info, self.frame_rate = self.get_wav_info(wav_file_path)
|
|
175
|
+
if not self.frame_rate:
|
|
176
|
+
return {"error": f"unknown format for file {wav_file_path}"}
|
|
177
|
+
except FileNotFoundError:
|
|
178
|
+
return {"error": f"File not found: {wav_file_path}"}
|
|
179
|
+
|
|
180
|
+
self.media_length = len(self.sound_info) / self.frame_rate
|
|
181
|
+
|
|
182
|
+
self.wav_file_path = wav_file_path
|
|
183
|
+
|
|
184
|
+
return {"media_length": self.media_length, "frame_rate": self.frame_rate}
|
|
185
|
+
|
|
186
|
+
def plot_spectro(self, current_time: float, force_plot: bool = False) -> tuple[float, bool]:
|
|
187
|
+
"""
|
|
188
|
+
plot sound spectrogram centered on the current time
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
current_time (float): time for displaying spectrogram
|
|
192
|
+
force_plot (bool): force plot even if media paused
|
|
193
|
+
"""
|
|
194
|
+
|
|
195
|
+
if not force_plot and current_time == self.time_mem:
|
|
196
|
+
return
|
|
197
|
+
|
|
198
|
+
self.time_mem = current_time
|
|
199
|
+
|
|
200
|
+
self.ax.clear()
|
|
201
|
+
|
|
202
|
+
window_type = "blackmanharris" # self.config_param.get(cfg.SPECTROGRAM_WINDOW_TYPE, cfg.SPECTROGRAM_DEFAULT_WINDOW_TYPE)
|
|
203
|
+
nfft = int(self.config_param.get(cfg.SPECTROGRAM_NFFT, cfg.SPECTROGRAM_DEFAULT_NFFT))
|
|
204
|
+
noverlap = self.config_param.get(cfg.SPECTROGRAM_NOVERLAP, cfg.SPECTROGRAM_DEFAULT_NOVERLAP)
|
|
205
|
+
vmin = self.config_param.get(cfg.SPECTROGRAM_VMIN, cfg.SPECTROGRAM_DEFAULT_VMIN)
|
|
206
|
+
vmax = self.config_param.get(cfg.SPECTROGRAM_VMAX, cfg.SPECTROGRAM_DEFAULT_VMAX)
|
|
207
|
+
|
|
208
|
+
# start
|
|
209
|
+
if current_time <= self.interval / 2:
|
|
210
|
+
self.ax.specgram(
|
|
211
|
+
self.sound_info[: int(self.interval * self.frame_rate)],
|
|
212
|
+
mode="psd",
|
|
213
|
+
NFFT=nfft,
|
|
214
|
+
Fs=self.frame_rate,
|
|
215
|
+
noverlap=noverlap,
|
|
216
|
+
window=signal.get_window(window_type, nfft),
|
|
217
|
+
# matplotlib.mlab.window_hanning
|
|
218
|
+
# if window_type == "hanning"
|
|
219
|
+
# else matplotlib.mlab.window_hamming
|
|
220
|
+
# if window_type == "hamming"
|
|
221
|
+
# else matplotlib.mlab.window_blackmanharris
|
|
222
|
+
# if window_type == "blackmanharris"
|
|
223
|
+
# else matplotlib.mlab.window_hanning,
|
|
224
|
+
cmap=self.spectro_color_map,
|
|
225
|
+
vmin=vmin,
|
|
226
|
+
vmax=vmax,
|
|
227
|
+
# mode="psd",
|
|
228
|
+
## NFFT=1024,
|
|
229
|
+
# Fs=self.frame_rate,
|
|
230
|
+
## noverlap=900,
|
|
231
|
+
# cmap=self.spectro_color_map,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
self.ax.set_xlim(current_time - self.interval / 2, current_time + self.interval / 2)
|
|
235
|
+
|
|
236
|
+
# cursor
|
|
237
|
+
self.ax.axvline(x=current_time, color=self.cursor_color, linestyle="-")
|
|
238
|
+
|
|
239
|
+
elif current_time >= self.media_length - self.interval / 2:
|
|
240
|
+
i = int(round(len(self.sound_info) - (self.interval * self.frame_rate), 0))
|
|
241
|
+
|
|
242
|
+
self.ax.specgram(
|
|
243
|
+
self.sound_info[i:],
|
|
244
|
+
mode="psd",
|
|
245
|
+
NFFT=nfft,
|
|
246
|
+
Fs=self.frame_rate,
|
|
247
|
+
noverlap=noverlap,
|
|
248
|
+
window=signal.get_window(window_type, nfft),
|
|
249
|
+
# matplotlib.mlab.window_hanning
|
|
250
|
+
# if window_type == "hanning"
|
|
251
|
+
# else matplotlib.mlab.window_hamming
|
|
252
|
+
# if window_type == "hamming"
|
|
253
|
+
# else matplotlib.mlab.window_blackmanharris
|
|
254
|
+
# if window_type == "blackmanharris"
|
|
255
|
+
# else matplotlib.mlab.window_hanning,
|
|
256
|
+
cmap=self.spectro_color_map,
|
|
257
|
+
vmin=vmin,
|
|
258
|
+
vmax=vmax,
|
|
259
|
+
# mode="psd",
|
|
260
|
+
## NFFT=1024,
|
|
261
|
+
# Fs=self.frame_rate,
|
|
262
|
+
## noverlap=900,
|
|
263
|
+
# cmap=self.spectro_color_map,
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
lim1 = current_time - (self.media_length - self.interval / 2)
|
|
267
|
+
lim2 = lim1 + self.interval
|
|
268
|
+
|
|
269
|
+
self.ax.set_xlim(lim1, lim2)
|
|
270
|
+
|
|
271
|
+
self.ax.xaxis.set_major_locator(mticker.FixedLocator(self.ax.get_xticks().tolist()))
|
|
272
|
+
self.ax.set_xticklabels([str(round(w + self.media_length - self.interval, 1)) for w in self.ax.get_xticks()])
|
|
273
|
+
|
|
274
|
+
# cursor
|
|
275
|
+
self.ax.axvline(x=lim1 + self.interval / 2, color=self.cursor_color, linestyle="-")
|
|
276
|
+
|
|
277
|
+
# middle
|
|
278
|
+
else:
|
|
279
|
+
self.ax.specgram(
|
|
280
|
+
self.sound_info[
|
|
281
|
+
int(round((current_time - self.interval / 2) * self.frame_rate, 0)) : int(
|
|
282
|
+
round((current_time + self.interval / 2) * self.frame_rate, 0)
|
|
283
|
+
)
|
|
284
|
+
],
|
|
285
|
+
mode="psd",
|
|
286
|
+
NFFT=nfft,
|
|
287
|
+
Fs=self.frame_rate,
|
|
288
|
+
noverlap=noverlap,
|
|
289
|
+
window=signal.get_window(window_type, nfft),
|
|
290
|
+
# matplotlib.mlab.window_hanning
|
|
291
|
+
# if window_type == "hanning"
|
|
292
|
+
# else matplotlib.mlab.window_hamming
|
|
293
|
+
# if window_type == "hamming"
|
|
294
|
+
# else matplotlib.mlab.window_blackmanharris
|
|
295
|
+
# if window_type == "blackmanharris"
|
|
296
|
+
# else matplotlib.mlab.window_hanning,
|
|
297
|
+
cmap=self.spectro_color_map,
|
|
298
|
+
vmin=vmin,
|
|
299
|
+
vmax=vmax,
|
|
300
|
+
# mode="psd",
|
|
301
|
+
## NFFT=1024,
|
|
302
|
+
# Fs=self.frame_rate,
|
|
303
|
+
## noverlap=900,
|
|
304
|
+
# cmap=self.spectro_color_map,
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
self.ax.xaxis.set_major_locator(mticker.FixedLocator(self.ax.get_xticks().tolist()))
|
|
308
|
+
self.ax.set_xticklabels([str(round(current_time + w - self.interval / 2, 1)) for w in self.ax.get_xticks()])
|
|
309
|
+
|
|
310
|
+
# cursor
|
|
311
|
+
self.ax.axvline(x=self.interval / 2, color=self.cursor_color, linestyle="-")
|
|
312
|
+
|
|
313
|
+
self.ax.set_ylim(self.sb_freq_min.value(), self.sb_freq_max.value())
|
|
314
|
+
"""self.figure.subplots_adjust(wspace=0, hspace=0)"""
|
|
315
|
+
|
|
316
|
+
self.canvas.draw()
|