phonoscape 0.1.0__tar.gz

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.
Files changed (48) hide show
  1. phonoscape-0.1.0/LICENSE +21 -0
  2. phonoscape-0.1.0/MANIFEST.in +7 -0
  3. phonoscape-0.1.0/PKG-INFO +15 -0
  4. phonoscape-0.1.0/README.md +452 -0
  5. phonoscape-0.1.0/phonoscape/__init__.py +74 -0
  6. phonoscape-0.1.0/phonoscape/__main__.py +157 -0
  7. phonoscape-0.1.0/phonoscape/data/parse.py +500 -0
  8. phonoscape-0.1.0/phonoscape/data/process.py +324 -0
  9. phonoscape-0.1.0/phonoscape/data/spectra.py +390 -0
  10. phonoscape-0.1.0/phonoscape/lproc/lp_default.py +5 -0
  11. phonoscape-0.1.0/phonoscape/lproc/lp_export_vals.py +5 -0
  12. phonoscape-0.1.0/phonoscape/lproc/lp_extents.py +5 -0
  13. phonoscape-0.1.0/phonoscape/lproc/lp_find_gest.py +526 -0
  14. phonoscape-0.1.0/phonoscape/lproc/lp_peaks.py +5 -0
  15. phonoscape-0.1.0/phonoscape/lproc/lp_phase_ang.py +5 -0
  16. phonoscape-0.1.0/phonoscape/lproc/lp_snap_ex.py +5 -0
  17. phonoscape-0.1.0/phonoscape/lproc/protocol.py +204 -0
  18. phonoscape-0.1.0/phonoscape/menu/data_menu.py +103 -0
  19. phonoscape-0.1.0/phonoscape/menu/file_menu.py +266 -0
  20. phonoscape-0.1.0/phonoscape/menu/label_menu.py +235 -0
  21. phonoscape-0.1.0/phonoscape/menu/menu_bar.py +34 -0
  22. phonoscape-0.1.0/phonoscape/menu/movement_menu.py +190 -0
  23. phonoscape-0.1.0/phonoscape/menu/play_menu.py +44 -0
  24. phonoscape-0.1.0/phonoscape/menu/selection_menu.py +63 -0
  25. phonoscape-0.1.0/phonoscape/menu/view_menu.py +187 -0
  26. phonoscape-0.1.0/phonoscape/modals/common_scaling_modal.py +155 -0
  27. phonoscape-0.1.0/phonoscape/modals/edit_labels_modal.py +137 -0
  28. phonoscape-0.1.0/phonoscape/modals/label_modal.py +143 -0
  29. phonoscape-0.1.0/phonoscape/modals/movement_config_modal.py +87 -0
  30. phonoscape-0.1.0/phonoscape/modals/spatial_view_modal.py +109 -0
  31. phonoscape-0.1.0/phonoscape/modals/spectral_analysis_modal.py +285 -0
  32. phonoscape-0.1.0/phonoscape/modals/temporal_config_modal.py +600 -0
  33. phonoscape-0.1.0/phonoscape/state.py +201 -0
  34. phonoscape-0.1.0/phonoscape/views/cursor_spect_view.py +153 -0
  35. phonoscape-0.1.0/phonoscape/views/spatial_view_2d.py +224 -0
  36. phonoscape-0.1.0/phonoscape/views/spatial_view_3d.py +276 -0
  37. phonoscape-0.1.0/phonoscape/views/temporal_view.py +577 -0
  38. phonoscape-0.1.0/phonoscape/views/zoomed_audio_view.py +118 -0
  39. phonoscape-0.1.0/phonoscape/widgets/play_button.py +79 -0
  40. phonoscape-0.1.0/phonoscape/widgets/readout.py +58 -0
  41. phonoscape-0.1.0/phonoscape/window.py +305 -0
  42. phonoscape-0.1.0/phonoscape.egg-info/PKG-INFO +15 -0
  43. phonoscape-0.1.0/phonoscape.egg-info/SOURCES.txt +46 -0
  44. phonoscape-0.1.0/phonoscape.egg-info/dependency_links.txt +1 -0
  45. phonoscape-0.1.0/phonoscape.egg-info/requires.txt +9 -0
  46. phonoscape-0.1.0/phonoscape.egg-info/top_level.txt +1 -0
  47. phonoscape-0.1.0/pyproject.toml +33 -0
  48. phonoscape-0.1.0/setup.cfg +4 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) Sida Chen.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,7 @@
1
+ prune baseline
2
+ prune phonoscape-fe-js
3
+ prune test_data
4
+
5
+ global-exclude __pycache__ *.py[cod] .DS_Store
6
+ global-exclude .pytest_cache .mypy_cache .ruff_cache .venv venv env
7
+ global-exclude *.egg-info
@@ -0,0 +1,15 @@
1
+ Metadata-Version: 2.4
2
+ Name: phonoscape
3
+ Version: 0.1.0
4
+ Requires-Python: >=3.14
5
+ License-File: LICENSE
6
+ Requires-Dist: amfm-decompy>=1.0.12.2
7
+ Requires-Dist: fastapi>=0.136
8
+ Requires-Dist: matplotlib>=3.10.9
9
+ Requires-Dist: numpy>=2.4
10
+ Requires-Dist: pydantic>=2.13
11
+ Requires-Dist: pyside6>=6.11.1
12
+ Requires-Dist: scipy>=1.17
13
+ Requires-Dist: sounddevice>=0.5.5
14
+ Requires-Dist: uvicorn[standard]>=0.47
15
+ Dynamic: license-file
@@ -0,0 +1,452 @@
1
+ # PhonoScape
2
+
3
+ Python port of MVIEW
4
+
5
+ ## Quick start
6
+
7
+ To try PhonoScape, clone the repository and install the dependencies:
8
+
9
+ ```bash
10
+ uv sync
11
+ source .venv/bin/activate
12
+ ```
13
+
14
+ Then execute the following command. You do need to have your own dataset file.
15
+
16
+ ```bash
17
+ python -m phonoscape ./test_data/S02_data.mat --palate S02_pal --temporal-display AUDIO_SPECT TDx vTDx TDz vTDz
18
+ ```
19
+
20
+ Prepackaged distributions will be available in the future.
21
+
22
+ ## Why PhonoScape?
23
+
24
+ PhonoScape strives to be 100% MVIEW-compatible: any input that works in MVIEW, probably works here; anything you can do in MVIEW, you probably can achieve here. Even the user interface and interactions should be familiar. Compare to MVIEW:
25
+
26
+ 1. No more MATLAB. No compatibility issues; no license required; no slow startup times and unfamiliar programming environments.
27
+ 2. Completely Command Line Interface (CLI) based; launch from your favorite terminal.
28
+ 3. More intuitive, consistent, and modern interface.
29
+ 4. Smoother interactions.
30
+ 5. Tons of bugs fixed, features implemented, and quality-of-life improvements made.
31
+
32
+ ## Still WIP
33
+
34
+ PhonoScape is 90% complete. The outstanding items you can find as TODOs below. In order of importance:
35
+
36
+ 1. [External procedures](#external-procedures)
37
+ 2. F0/Formant tracking
38
+ 3. File export (configuration, data, etc.)
39
+ 4. Dataset import: `LABELS`, `CONTOURS`, `SPREAD` fields
40
+ 5. Circle-fitting (for tongue shape)
41
+ 6. Small things: select playback track, auto-update/manual-update, fix entire window export layout
42
+
43
+ ## Central concepts
44
+
45
+ - **Dataset**: although not hard-enforced, it is highly, highly recommended that each time PhonoScape is opened with a `.mat` file for one subject, using the same data collection procedure. Many things are shared across windows: the palate/pharynx trace, metadata about the trajectories, the spatial bounds, 2D/3D configuration, etc. If your variables are not consistent in their semantics, PhonoScape _will break_.
46
+ - **Variable**: your `.mat` file can contain many variables. Each window is associated with one variable. Each variable represents one condition. You may wish to chop up each recording session's data into many variables for easier manipulation.
47
+ - **Trajectory**: for the exact storage format, see [dataset format](#dataset-format). At a high level, these trajectories are assumed to be synchronized (sampling rate taken into account). We differentiate between three kinds of trajectories: audio (scalar, sampling rate ≥ 5000 Hz), physiological scalar (scalar, sampling rate < 5000 Hz), and spatial (2D or 3D). This determines how they can each be analyzed temporally. Each variable can have one "privileged" audio trajectory; this one is used for audio playback and spectral analysis.
48
+ - **Cursor**: TODO
49
+ - **Selection**: TODO
50
+ - **Label**: TODO
51
+
52
+ ## Command-line arguments
53
+
54
+ You can run `python -m phonoscape --help` to see the full list of command-line arguments.
55
+
56
+ - First, you need to provide a `file` argument pointing to a `.mat` [dataset file](#dataset-format).
57
+ - Then, you can optionally provide a `variables` pattern. It uses [`fnmatch`](https://docs.python.org/3/library/fnmatch.html), so you can use `*` to match a sequence of characters and `?` to match a single character. If no variable pattern is provided, then all variables in the dataset file are used.
58
+ - Finally, you can provide any number of options:
59
+ - `--palate VAR` (MVIEW `PALATE`): use the variable `VAR` (in the `file`) to plot a palate curve in the spatial view. If specified, the variable must contain a `[n_samples × n_dims]` array of palate points. If unspecified, the variable `pal` is used if it exists and contains data in the required shape; otherwise no palate is plotted.
60
+ - `--pharynx VAR` (MVIEW `PHARYNX`): use the variable `VAR` (in the `file`) to plot a pharynx curve in the spatial view. If specified, the variable must contain a `[n_samples × n_dims]` array of pharynx points. However, per MVIEW compatibility, if the pharynx trace is 2D and the spatial data is 3D, an extra column of zeros will be added as the y-axis. If unspecified, the variable `pha` is used if it exists and contains data in the required shape; otherwise no pharynx is plotted.
61
+ - `--spline TRAJ1 TRAJ2 ...` (MVIEW `SPLINE`): specifies that the trajectories `TRAJ1`, `TRAJ2`, etc. (in each variable) should have a spline fitted in the spatial view. If specified, all names must refer to spatial trajectories. If unspecified, then all spatial trajectories with name starting with `T` are used as default. This is slightly different from MVIEW which disables spline by default, but in the command line it's hard to differentiate between `[]` (MVIEW "use `T*`") and `0` (MVIEW "disable"). If you want to hide the spline, just use the [**Hide spline**](#view-menu) action.
62
+ - `--polyline-spline` (MVIEW `SPLINE` with a negative first index): specifies that the spline should be plotted as a polyline instead of a smooth curve. Might improve real-time update performance.
63
+ - `--audio TRAJ`: specifies that the trajectory `TRAJ` (in each variable) contains audio data. If specified, the name must refer to a scalar trajectory. If unspecified, then the first audio trajectory is used as the default. If no such trajectory exists, then all audio-related features (spectrogram time-slice, playback, etc.) are disabled.
64
+ - `--framing TRAJ` (MVIEW `FTRAJ`): specifies that the trajectory `TRAJ` (in each variable) should be used for temporal framing. If specified, the name must refer to a scalar trajectory. If unspecified, then the audio trajectory is used as the default framing trajectory, and if no audio trajectory is found, then the first trajectory of any kind is used as the framing trajectory.
65
+ - `--temporal-display SPEC1 SPEC2 ...` (MVIEW `TEMPMAP`): a list of temporal view specifications. Each specification specifies one temporal plot. See [temporal view](#temporal-view) for more details regarding their presentation.
66
+ - If designating an audio trajectory (sampling rate ≥ 5000 Hz): `TRAJ` or `TRAJ_MODIFIER`, where `MODIFIER` can be: `SPECT` (spectrogram), `RMS` (root mean square), `ZC` (zero-crossing rate), `F0` (fundamental frequency). **Note**: unlike MVIEW, we have a strict separation of audio and physiological scalar trajectories.
67
+ - If designating a scalar trajectory (sampling rate < 5000 Hz): `TRAJ` or `TRAJ_MODIFIER`, where `MODIFIER` can be: ``VEL` (velocity), `ABSVEL` (absolute velocity).
68
+ - If designating a spatial trajectory: `TRAJ`, optionally prefixed by `v` or `a`, and optionally suffixed by a subset of `xyz`, indicating if the trajectory should be movement, velocity, or acceleration, and which dimensions to plot. Specifying `xyz` is equivalent to no specification.
69
+
70
+ These specification names can also be obtained from the "Temporal layout" dialog. By default, all trajectories are included in the temporal display; if audio is available, then its signal and `SPECT` trajectories are always ordered as the first two.
71
+
72
+ - `--spatial-exclude TRAJ1 TRAJ2 ...` (MVIEW `SPATEX`): a list of trajectory names to exclude from the spatial view (they are still included in every other analysis, including the temporal view). If unspecified, then no trajectory is excluded. Unlike MVIEW, wildcards are not supported; just pass each trajectory name individually.
73
+ - `--comps COL1 COL2 ...` (like MVIEW `IS3D`, but better): equivalent to the dataset `NCOMPS` [field](#dataset-format), but used as a global fallback when the `NCOMPS` field is absent. This global configuration is also used for non-trajectory data, such as palate trace. A single number `N` is equivalent to `0 .. N-1`.
74
+ - `--view ELEV AZIM ROLL` (like MVIEW `VIEW`, but matplotlib conventions): the initial view of the spatial display, in terms of elevation, azimuth, and roll (in degrees). This is only used if the data is 3D. If unspecified, it defaults to `(0, -90, 0)` (i.e., sagittal section, right view). See [spatial view](#view-menu) for more details.
75
+ - `--head MS` (MVIEW `HEAD`): the position (in milliseconds) of the left edge of the temporal selection. If specified, it must be a non-negative number less than `--tail - 25`. If unspecified, it defaults to the start of the data (0 ms). It only has an effect on the first opened window; subsequent windows inherit the opening window's selection.
76
+ - `--tail MS` (MVIEW `TAIL`): the position (in milliseconds) of the right edge of the temporal selection. If specified, it must be a non-negative number greater than `--head + 25`. If unspecified, it defaults to the end of the first variable's data. It only has an effect on the first opened window; subsequent windows inherit the opening window's selection.
77
+ - `--sex SEX` (MVIEW `SEX`): the subject's sex. `M` for male; `F` for female. This can be later customized in the 'Configure spectral analysis' dialog, but it affects the default LPC degree. Default is `M`.
78
+ - `--spect-lim HZ` (MVIEW `SPECLIM`): frequency upper limit (Hz) for spectrogram display. This affects the visualization of the spectrograms but does not affect the underlying spectral analysis. Default is the Nyquist frequency.
79
+ - `--lproc PROC` (MVIEW `LPROC`): specifies the label procedure to use. If unspecified, then the default label procedure `phonoscape.lproc.lp_default` is used. If specified, it must be either a Python module specifier (qualified name based on `sys.path`) or a path (ending in `.py`, resolved relative to CWD) to a module starting with `lp_`. See [labeling procedures](#labeling-procedures) for more details.
80
+
81
+ Note that the framing and audio trajectory discovery is slightly different from MVIEW's; for one, we never special-case the very first trajectory.
82
+
83
+ The `NAME`, `VLIST` and `VLSEL` arguments are not supported because all of their downstream effects can be easily customized, and they interact poorly with other features (e.g., "Variables" menu, the `variables` argument, etc.). Please let me know if you have a specific use case.
84
+
85
+ Other forms of `mview` data loader, including struct data and raw numeric arrays, are not supported. To pass a variable list, simply use a glob pattern like `{var1,var2}`. Other `mview` invocations including no-arg foregrounding and `abort` are also not supported because you shouldn't be interacting through the terminal.
86
+
87
+ ## Dataset format
88
+
89
+ The `.mat` file contains three levels:
90
+
91
+ - Variables: each PhonoScape window views one variable.
92
+ - Trajectories: each variable contains one or more trajectories, represented as an array of structs.
93
+ - Fields: each trajectory struct has required and optional fields.
94
+
95
+ Only variables that contain structs are considered valid variables. Other variables (those that contain plain arrays) are considered supplementary data. They are only useful if some other [argument](./arguments.md) refers to them:
96
+
97
+ - `--palate`: looks for a variable containing a `[n_samples × n_dims]` array of palate points.
98
+ - `--pharynx`: looks for a variable containing a `[n_samples × n_dims]` array of pharynx points.
99
+
100
+ The following fields are required for each trajectory struct:
101
+
102
+ - `NAME` (string): trajectory name. Will be uppercased and stripped of underscores. The actual names absolutely do not matter with one quirk: if the `--spline` [argument](#command-line-arguments) is not provided, all spatial trajectories with name starting with `T` will be used as default.
103
+ - `SRATE` (float): sampling rate in Hz. The actual sampling rate value only matters in one place: trajectories with `SRATE` ≥ 5000 Hz are considered audio trajectories, and those with `SRATE` < 5000 Hz are considered physiological trajectories.
104
+ - `SIGNAL` (array `[n_samples × n_dims]`): 1D is scalar, 2D or 3D is spatial. Higher dimensions are truncated off and moved into `ANGLES`; if `ANGLES` is also provided, then the extra dimensions are ignored and `ANGLES` is used instead. Currently only 3D is well-supported for spatial trajectories.
105
+
106
+ All trajectories in a variable are expected to have roughly the same `n_samples * SRATE`. When displaying, the minimum duration is used (i.e., where all trajectories have data).
107
+
108
+ For 3D data, the following axis-component mapping is preferred (configurable via the `--comps` [argument](#command-line-arguments)):
109
+
110
+ - x-axis is sagittal (posterior = negative, anterior = positive)
111
+ - y-axis is transverse (right = negative, left = positive)
112
+ - z-axis is longitudinal (inferior = negative, superior = positive).
113
+
114
+ The following optional fields are effectively variable-wide metadata. Only the first struct's field is used.
115
+
116
+ - `LABELS` (data type TODO): TODO. The `--labels` [argument](#command-line-arguments) takes priority over this. Only the first struct's `LABELS` field is used.
117
+ - `CONTOURS` (data type TODO): TODO.
118
+
119
+ The following optional fields may be provided for each trajectory struct:
120
+
121
+ - `COLOR` (array `[1 × 3]`, each in range `[0, 1]`): Specifies the RGB color. If unspecified, scalar trajectories become text-color and spatial trajectories get an arbitrary color. If specified, this value takes top priority on window initialization. Can later be customized through [temporal layout](#view-menu).
122
+ - `SPREAD` (array `[1 × 2]`): TODO.
123
+ - `NCOMPS` (number or array `[1 × n_dims]`):
124
+ - If a number, must be either 2 or 3. If 3, then the trajectory data must have at least 3 dimensions, and the first 3 are used as XYZ coordinates for the spatial view. This is practically useless, though, because it can be inferred. So the only useful case is if `NCOMPS` is 2 but the trajectory data has 3 dimensions. In that case, only the first 2 dimensions are used for the spatial view, and the 3rd dimension is treated as `ANGLES`.
125
+ - If an array, specifies the column indices of the trajectory data for each dimension. For example, if the trajectory has 3 dimensions but `NCOMPS` is `[0, 2]`, then the 1st and 3rd dimensions (which are normally `x` and `z`) are used for the spatial view, and the 2nd dimension is treated as `ANGLES`. You may also use it to reorder dimensions, e.g., `[0, 2, 1]` uses the data's `z`-axis as the spatial `y`-axis and the data's `y`-axis as the spatial `z`-axis. It must contain no duplicates and all indices must be valid for the trajectory data dimensions.
126
+ - If not provided, then it's inferred from `SIGNAL.shape[1]`, with the first 3 dimensions used for the spatial view (in `x`, `y`, `z` order) and any extra dimensions treated as `ANGLES`.
127
+ - `ANGLES` (array `[n_samples × n_extra]`): Only used by external data procedures.
128
+ - TODO: `AUDIO` (boolean)?
129
+
130
+ ## Menu bar
131
+
132
+ ### File menu
133
+
134
+ - **Variables**: Only available when viewing >1 variables. Each variable opens in a new window. The first variable is selected by default. We ensure that the same variable can only be opened in one window at a time. Opening a new window inherits most of the current configuration, _except_ the cursor position which is always reset to the beginning.
135
+ - **Previous (Ctrl+1)**: Opens the previous variable in the order they appear in the dataset. Cycles across the boundary (slightly different from MVIEW).
136
+ - **Next (Ctrl+2)**: Opens the next variable in the order they appear in the dataset. Cycles across the boundary (slightly different from MVIEW).
137
+ - **Next; close current (Ctrl+3)**: As it says.
138
+ - **Next plus export (Ctrl+4)**: TODO
139
+ - **Next; export, close current (Ctrl+5)**: TODO
140
+ - **Next; save labels, close current (Ctrl+6)**: TODO
141
+ - **Next; export/save labels, close current (Ctrl+7)**: TODO
142
+ - **Save**: TODO
143
+ - **Export**: TODO
144
+ - **Open figure**: Opens one of the figures in a new window with matplotlib tools (configuring dimensions, save as file, etc.), like what you get with `plt.show()`; usually for the purpose of exporting the figure, but also useful for detailed inspection. This is equivalent to MVIEW's "Duplicate window".
145
+ - **Temporal view**: Open the right panel only.
146
+ - **Spatial view**: Open the top-left panel only.
147
+ - **Entire window**: Clone all plots in the current window as a single matplotlib figure. Currently the layout is broken.
148
+ - **Close window (Ctrl+W)**: Closes the current window only.
149
+ - **Close all**: Closes all variable windows, which should quit the application.
150
+
151
+ ### Data menu
152
+
153
+ - **Report**: Prints data at the cursor location to the terminal. Format:
154
+
155
+ ```plain
156
+ {Variable name}: cursor @ {cursor} ms; selection is [{head} {tail}] ({duration}) ms
157
+ Window {length} ms: {x} zero crossings, RMS = {x} ({x} dB), F0 = {x} Hz, L1 = {x}, skew = {x}, kurt = {x}
158
+ Formants (BW): F1 = {loc} ({bw}) F2 = {loc} ({bw}) ...
159
+ Traj: TDx TDy ...
160
+ Vals: -59.3 -7.6 ...
161
+ ```
162
+
163
+ The second row only shows analysis from the "privileged" audio track, if it exists. The analysis window length is used by all analyses on this row. The spectral center of gravity (COG) analysis (L1, skewness, kurtosis) uses some additional parameters: # FFT eval points, spectral display cutoff, pre-emphasis, and averaging window and overlap (only if the algorithm is averaging). In MVIEW, all of these parameters are hard-coded. Currently the algorithm is fixed to be averaging (instead of windowing) and the spectrum is fixed to be magnitude (instead of power).
164
+
165
+ Note that the "Traj" list outputs at least one column for each temporally displayed trajectory (except raw audio signals and spectrograms; unlike MVIEW, analyzed trajectories of non-"privileged" audio tracks are still reported here), depending on which dimensions are being viewed.
166
+
167
+ TODO: LaTeX/Markdown/CSV/JSON/Excel-paste-compatible output.
168
+
169
+ - **Track formants (Ctrl+J)**: TODO
170
+ - **Spectral analysis (Ctrl+A)**: Opens a dialog to configure the spectrogram parameters. These parameters may affect: the cursor spectrum in the bottom left, the temporal analysis in the temporal view (`SPECT`, `RMS`, `ZC`, `F0`), and the **Report** action. Note that the "nudge" setting has been moved to the ["Configure movement"](#movement-menu) dialog.
171
+ - **Active analyses** (default: LPC): TODO
172
+ - **Analysis window (ms)** (default: 30ms): Configures the window size for the `RMS` and `ZC` temporal analyses (unlike MVIEW which uses a fixed window), the window size for the **Report** output, and the cursor spectrum. The `SPECT` temporal analysis uses the **Averaging window** instead.
173
+ - **Number of LPC coeffs** (default: `audio_sampling_rate / 1000 + 8` if female, otherwise `audio_sampling_rate / 1000 + 4`): Configures the LPC analysis (if enabled) in the cursor spectrum.
174
+ - **# FFT eval points** (default: 256): Configures the frequency resolution of all relevant spectral analyses. Unlike MVIEW (in MATLAB fewer FFT eval points silently truncates the window), the number of FFT samples must be at least the window sample size.
175
+ - **Averaging window (ms)** (default: 6ms): Configures the window size for the `SPECT` (I don't think this is the best way but this is how it is in MVIEW) and the AVG analysis (if enabled).
176
+ - **Overlap (ms)** (default: 1ms): Configures the window shift for the `SPECT` and the AVG analysis (if enabled).
177
+ - **SPL reference (μPa)** (default: 20μPa): Reference amplitude used when converting cursor/external spectra to dB. MVIEW's default is 20, corresponding nominally to 20µPa, but this is only physically meaningful for calibrated pressure signals. For ordinary digital audio, it acts as a vertical dB normalization/plotting offset.
178
+ - **Spectral display cutoff (Hz)** (default: `audio_sampling_rate / 2`): Affects visualization only. Configures the ymax of `SPECT` and the xmax of the time-slice spectrograms.
179
+ - **Pre-emphasis** (default: 0.98): If the **(Adaptive)** checkbox is checked, then the pre-emphasis coefficient is automatically determined by the signal (by computing the lag-1 autocorrelation). Otherwise, the specified coefficient is used (which should be between 0 and 1). Affects cursor spectrum only (TODO: this way is for MVIEW compatibility; `SPECT` uses hard-coded first-difference. I think this config should apply there too.)
180
+ - **Subject gender** (default: `--sex` [argument](#command-line-arguments)): Affects F0 heuristics.
181
+ - **Spectrogram** (default: wide): Acts as a multiplier for **Averaging window** (and affects `SPECT` only). **Wide** = 1, **Mid 1** = 2, **Mid 2** = 3, **Narrow** = 4. The longer the window, the better the frequency resolution but poorer the temporal resolution.
182
+
183
+ ### View menu
184
+
185
+ - **Temporal layout (Ctrl+C)**: Configure which trajectories to display in the temporal view. It launches a dialog where you can:
186
+ - Select available trajectories from the left-hand side to add (`>`) to the display area on the right-hand side.
187
+ - Select displayed trajectories from the right-hand side to remove (`x`) from the display area.
188
+ - Select one displayed trajectory from the right-hand side to reorder (`^` and `v`).
189
+ - Select one displayed trajectory from the right-hand side to customize its content. For spatial trajectories, you can choose to display position, velocity, or acceleration, and which dimensions to display. For scalar trajectories, you can choose to apply a temporal analysis (spectrogram, RMS, zero-crossing rate, fundamental frequency, etc.).
190
+ - Select one trajectory from either side to customize its color. Unlike MVIEW, you can customize colors from the left-hand size too, which means you can customize a color in the spatial view even if you don't load it into the temporal view.
191
+ - Select a spectrogram to customize its color contrast. This is equivalent to the MVIEW vertical slider in the bottom left. The spectrogram is power-law normalized. The higher the setting, the higher the gamma parameter, and the sharper the contrast.
192
+
193
+ The list of names on the right-hand side are known as "temporal display specifications". They are also used in the `--temporal-display` [argument](#command-line-arguments) and the "Report" output.
194
+ - **Set common scaling**: Opens a dialog to configure the y-axis limits for all spatial trajectories.
195
+ - **Adaptive scaling**: Each trajectory has its own y-axis limits, determined by the range of the trajectory data (i.e., matplotlib default auto-scaling logic). New in PhonoScape.
196
+ - Configured spreads: Configure a common spread for all movement/velocity/acceleration trajectories, respectively. The `ymax - ymin` will be equal to that value, leaving an equal margin on the top and bottom.
197
+
198
+ By default adaptive scaling is enabled and you cannot configure spreads. When you disable it, the spreads default to `1.1` of the maximum range across all _visible_ trajectories (different from MVIEW, which also considers invisible trajectories and therefore can end up with an excessively large common scale). The values you set (or default) are good for as long as the temporal display remains the same or adaptive scaling remains disabled; the next time you enable and re-disable adaptive scaling, or when you change the temporal display settings, the common scaling will be re-computed.
199
+
200
+ - **Hide spline**: Only available when the `--spline` [argument](#command-line-arguments) (including its default value) specifies a non-empty set. Toggles the display of the spline curve.
201
+ - **Spatial history**: Configures whether and how the spatial trajectories are plotted. By default it's **None**, meaning that only the current-time spatial positions are shown. When **History** is selected, each trajectory has additionally a curve showing its full spatial movement path in the selection. When **Hue** is selected, the same movement path is shown, but colored using a hue map showing the temporal progress instead of using the trajectory's color. This menu used to be available in the right-click context menu. This is significantly different from how MVIEW works, which plots one set of paths each time "Hue" or "History" is clicked, allowing multiple selections to be viewed at once; I personally think my way is clearer, but let me know if that capability is useful.
202
+ - **Spatial 3D view**: Only available when the display is 3-dimensional. Configure the view camera angle.
203
+ - **2D/3D view (1/2/3)**: 6 predefined camera positions. These options are visually consistent with MVIEW, but MATLAB uses different specification conventions from matplotlib, so the physical parameters are different.
204
+
205
+ | Name | Elevation | Azimuth | Roll | Behavior |
206
+ | ----------- | --------- | ------- | ---- | --------------------------------- |
207
+ | 2D view (1) | 90 | 0 | 90 | Transverse section, superior view |
208
+ | 2D view (2) | 0 | -90 | 0 | Sagittal section, right view |
209
+ | 2D view (3) | 0 | -180 | 0 | Coronal section, anterior view |
210
+ | 3D view (1) | 18 | 20 | 0 | Left anterosuperior view |
211
+ | 3D view (2) | -30 | -62.5 | 0 | Left posteroinferior view |
212
+ | 3D view (3) | -20 | -117 | 0 | Left anteroinferior view |
213
+
214
+ - **Specify view**: Opens a dialog to configure your own elevation/azimuth/roll parameters.
215
+ - **Free rotate**: Toggles whether the spatial view can be freely rotated by dragging with the mouse.
216
+
217
+ TODO: not sure if I should implement circle-fitting.
218
+
219
+ ### Play menu
220
+
221
+ Only available if an audio trajectory exists.
222
+
223
+ - **Play (Ctrl+P)**: Play the audio at the specified interval:
224
+ - **Selection**: Between head and tail.
225
+ - **Entire file**: From the start to the end of the data.
226
+ - **To cursor**: Between head and the cursor. If cursor is before head, then play selection. This is different from MVIEW, which plays nothing.
227
+ - **From cursor**: Between cursor and tail. If cursor is after tail, then play selection. This is different from MVIEW, which plays nothing.
228
+ - **150ms @ cursor**: 150ms centered at the cursor, clamped to the selection (different from MVIEW; if you want the MVIEW behavior, slightly expand your selection).
229
+ - **Between labels**: Only has an effect if the cursor is between two labels. Plays the audio between the previous and next labels.
230
+ - **Select playback track (Ctrl+8)**: TODO
231
+
232
+ Note that the behavior configured here applies to the "Play" button in the navbar as well.
233
+
234
+ ### Selection menu
235
+
236
+ Currently a minimum of 25ms is enforced for the selection duration, regardless of sampling rate (slightly different from MVIEW). Customization will be allowed.
237
+
238
+ - **Set head to cursor (Ctrl+D)**: As it says; clamped to `tail - 25ms`.
239
+ - **Set tail to cursor (Ctrl+T)**: As it says; clamped to `head + 25ms`.
240
+ - **Set selection to label pair**: Only has an effect if the cursor is between two labels. Sets head to the previous label and tail to the next label. The selection is at least 25ms long and is centered at the midpoint (or touches the start/end boundaries of the data).
241
+ - **Reset selection**: Sets the selection to the whole data.
242
+ - **Shrink selection**: Shrinks the selection by 10% on each side, keeping the center fixed. The selection is at least 25ms long.
243
+ - **Expand selection**: Expands the selection by 10% on each side, keeping the center fixed. The selection is at most the whole data. If one end is out of bounds, the selection is shifted to fit instead of truncated (unless it cannot fit).
244
+ - **Shift selection left (Ctrl+L)**: Shifts the whole selection to the left by its width (until it touches the start of the data), keeping the width fixed.
245
+ - **Shift selection right (Ctrl+R)**: Shifts the whole selection to the right by its width (until it touches the end of the data), keeping the width fixed.
246
+ - **Auto-update**: TODO
247
+ - **Update (Ctrl+U)**: TODO
248
+
249
+ ### Movement menu
250
+
251
+ - **Step forward (Ctrl+F)**: Moves the cursor forward by the nudge step size, bound by the selection.
252
+ - **Step backward (Ctrl+B)**: Moves the cursor backward by the nudge step size, bound by the selection.
253
+ - **Shift forward/backward**: Shifts the selection just like "Shift selection right/left", but keeps the cursor at the same relative position inside the selection. If the cursor is not inside the selection, it is set to the start of the selection.
254
+ - **Cycle forward/backward**: Continuously shifts the cursor forward/backward, wrapping around the selection boundary.
255
+ - **Reflective cycling**: Continuously shifts the cursor forward; if it hits one boundary, moves in the opposite direction.
256
+ - **Stop cycling (Ctrl+X)**: As it says.
257
+ - **Configure movement**: Opens a dialog to configure the movement behavior.
258
+ - **Nudge step size (ms)** (default: 5ms): The amount of time to move when stepping forward/backward, in milliseconds. (In MVIEW this also controls the cycling; in PhonoScape you use playback rate instead.)
259
+ - **Playback rate** (default: 1; i.e., synchronized with real time): How fast the simulated motion is relative to real time when cycling. For example, if the playback rate is 2, then the cursor moves twice as fast as real time, so a 10-second selection would take 5 seconds to cycle through.
260
+
261
+ While cycling, the [read-out panel](#read-out) displays the actual nudge step size and frame rate of the simulated motion. The frame rate mostly depends on how fast your computer can process each cursor update. On my M1 mac, it's around 10 FPS, so at 1x playback, each nudge step is approximately 100 ms (sorry Python isn't really efficient for real-time stuff). You can improve the temporal resolution by reducing the playback rate.
262
+
263
+ ### Label menu
264
+
265
+ Unlike MVIEW, labels are ordered by their creation, not by their temporal position; this affects most actions below. However, sorting and reordering is allowed if you prefer some other ordering.
266
+
267
+ - **Make label**: Opens a dialog to add a new simple label at the end of the labels list. By default it's at the cursor position. Unlike MVIEW: (1) this always only adds a simple label and doesn't invoke the labeling procedure; (2) a name is required.
268
+ - **Edit labels**: Opens a dialog to edit the labels list. Each label has a name, a position (in ms), and an optional note. You can delete and reorder labels. You can also edit the name, position, and note of each label. The position must be between the start and the end of the data.
269
+ - **Clear all labels (Ctrl+Y)**: As it says. Shows a confirmation dialog.
270
+ - **Export labels (Ctrl+9)**: Exports labels to a `.lab` text file. The format is the same as MVIEW:
271
+
272
+ ```plain
273
+ LABEL OFFSET NOTE
274
+ {name} {ms} {optional note}
275
+ ```
276
+
277
+ All separators are tabs.
278
+
279
+ - **Import labels**: Imports labels from a `.lab` text file. The format is the same as MVIEW (see above). The imported labels are appended to the labels list. Only the `LABEL` and `OFFSET` columns are considered; any subsequent text is ignored (including the note).
280
+ - **Save labels**: Saves the labels to the app shared memory (analogous to the MATLAB workspace), with an associated name.
281
+ - **Load labels**: Loads the labels from the app shared memory. The loaded labels replace the current labels.
282
+ - **Labeling behavior (Ctrl+K)**: TODO
283
+
284
+ ## UI elements
285
+
286
+ ### Navbar
287
+
288
+ The **Play** button has the exact same functionality as the one in the [play menu](#play-menu).
289
+
290
+ You can read off and edit the cursor, head, and tail positions, all in milliseconds.
291
+
292
+ ### Temporal view
293
+
294
+ The temporal view is the right panel. It displays the trajectories as time series, with time on the x-axis and the trajectory value(s) on the y-axis. The first plot is the _framing trajectory_ specified with the `--framing` [argument](#command-line-arguments). It always displays the full data. The current selection is highlighted. The cursor and labels are also visible.
295
+
296
+ You can drag the selection in the framing trajectory to shift it, or drag its boundaries to resize. Double-clicking resets it to the entire data.
297
+
298
+ All remaining plots are the _temporal display trajectories_ specified with the `--temporal-display` [argument](#command-line-arguments) and customizable via the ["Temporal layout" dialog](#view-menu). They only display the data within the current selection. The cursor and labels are also visible, should they be inside the selection.
299
+
300
+ The trajectories' colors are initialized by the `COLOR` field in the dataset or otherwise arbitrarily. You can select colors through [temporal layout](#view-menu). If the data is multidimensional, each dimension is plotted separately, with x being the most opaque and z being the most transparent (with a legend).
301
+
302
+ Following MVIEW behavior, the velocity/acceleration of the whole vector (e.g., `vTD`) only displays the magnitude (and is therefore unsigned), while the velocity/acceleration of specific dimensions (e.g., `vTDx`, `vTDxy`) displays each separate dimension (and is signed).
303
+
304
+ If multiple curves are plotted, each curve will be re-centered (`curve - (max(curve) + min(curve)) / 2`) to avoid inflating the y-range.
305
+
306
+ By default, each axis' y-range is adaptive to the range of the trajectory data (using matplotlib's default auto-scaling). You can also configure [common scaling](#view-menu) for spatial trajectories, so that all movement/velocity/acceleration trajectories each share the same y-span.
307
+
308
+ If a single curve is plotted and it is not movement (i.e., scalar, velocity, or acceleration), the zero line will be indicated as a dashed line should it be within the y-span of the plot.
309
+
310
+ Unlike MVIEW, all scalar trajectories with sampling rate >5000Hz—not just audio—support spectrogram display (this was nominally supported in MVIEW but in reality it seems to be sketchy). Short Time Fourier Transform (STFT) is used. You can customize its data using the following [spectral analysis](#data-menu) options:
311
+
312
+ - **# FFT eval points**
313
+ - **Averaging window**
314
+ - **Overlap**
315
+ - **Spectral display cutoff**
316
+ - **Spectrogram**
317
+
318
+ You can also configure its contrast using the [temporal display configuration](#view-menu).
319
+
320
+ You can click in the temporal display trajectories to set the cursor, or drag to move the cursor. You can also drag a label to move it, or double-click one to edit it. You can right-click to create a label at the position (this invokes the [labeling procedure](#labeling-procedures)).
321
+
322
+ ### Spatial view
323
+
324
+ The spatial view is the top-left panel. It displays the locations of all spatial signals at the cursor position. It is not affected by which ones are temporally displayed; to exclude certain signals, you can use the `--spatial-exclude` [argument](#command-line-arguments). The plot is 2D or 3D depending on the data's dimensions (configurable via the `--comps` [argument](#command-line-arguments)).
325
+
326
+ The sensors' colors are consistent with the temporal trajectories' colors.
327
+
328
+ The axes' ranges are set to contain all spatial movements across all variables, not just the current variable (unlike MVIEW).
329
+
330
+ If the `--palate` or `--pharynx` [argument(s)](#command-line-arguments) are configured, they will be plotted in the spatial view as lines.
331
+
332
+ If the `--spline` [argument](#command-line-arguments) is configured, the spline curve (as a polyline if `--polyline-spline` is also specified) will be plotted in the spatial view. You can configure its display via the [**Hide spline**](#view-menu) action.
333
+
334
+ The spatial view optionally shows the movement path of the trajectories within the selection as curves, either in the trajectory color or colored by temporal progress. You can configure this via the [**Spatial history**](#view-menu) menu. (Again, the behavior is different from MVIEW.)
335
+
336
+ When the plot is 3D, you can customize the camera via the [spatial options](#view-menu) or the `--view` [argument](#command-line-arguments). When "Free rotate" is enabled, you can drag the plot to rotate the camera. The current camera angle is shown in the [read-out panel](#read-out) at the bottom left.
337
+
338
+ On right-click, the context menu is no longer shown. Configuration of the spatial history has been moved to the [View menu](#view-menu).
339
+
340
+ ### Cursor spectrum
341
+
342
+ The cursor spectrum is the bottom-left panel. It shows the frequency spectrum of the audio signal at the cursor position, computed using the parameters configured in [spectral analysis](#data-menu):
343
+
344
+ - **Active analyses**
345
+ - **Analysis window (ms)**
346
+ - **Number of LPC coeffs** (LPC-only)
347
+ - **# FFT eval points**
348
+ - **Averaging window (ms)** (AVG-only)
349
+ - **Overlap (ms)** (AVG-only)
350
+ - **SPL reference (μPa)**
351
+ - **Spectral display cutoff (Hz)**
352
+ - **Pre-emphasis**
353
+
354
+ All 4 modes are supported and displayed in the same panel: Linear Predictive Coding (LPC), Discrete Fourier Transform (DFT), Average (AVG), Cepstral (CEPS). By default only LPC is enabled. In MVIEW, only the former two are displayed in the main panel; in PhonoScape, all of them are, rendering the external spectrum unnecessary.
355
+
356
+ The x-axis limit is from 0 to the value configured by **Spectral display cutoff**. The y-axis is in dB, whose limits expand as the plot updates (i.e., as you move the cursor). The way to reset the limit is to re-adjust the [SPL reference](#data-menu) which effectively shifts the curve.
357
+
358
+ It also includes a small zoomed view of the audio signal around the cursor. This is not the same as the MVIEW's plot at this position: instead, it's more similar to the audio clip shown when right-clicked on the cursor spectrum (however, also unlike MVIEW, the clip strictly only includes `[cursor - window / 2, cursor + window / 2]`, while MVIEW includes `[cursor - window, cursor + window]`). The zoom window is always synchronized with the analysis window, so you know the raw signal that's submitted for spectral analysis. For this reason, you must configure its window size through [spectral analysis](#data-menu) **Analysis window** instead of using the right-click context menu or a separate slider. In addition to the audio signal, it shows the cursor (centered, green dash line) and the Hann window curve (yellow).
359
+
360
+ The vertical slider here that customizes the spectrogram contrast has been moved to [Temporal layout](#view-menu).
361
+
362
+ ### Read-out
363
+
364
+ At the very bottom-left, there is a small panel. What it displays depends on what you just did.
365
+
366
+ 1. If you just clicked on a temporal trajectory, it shows the trajectory name (new in PhonoScape!) and the value at the cursor. The text is not cleared when the mouse is released or moved outside; it is persisted so you can copy it out.
367
+ 2. If you just panned the 3D spatial view, it shows the current camera angle in terms of elevation, azimuth, and roll. In MVIEW, a separate message box is used for this.
368
+ 3. If you are [cycling](#movement-menu) through the selection, it shows the frame rate, playback rate, and effective nudge step size of the simulated motion.
369
+
370
+ The cursor/head/tail inputs have been moved to the [navbar](#navbar).
371
+
372
+ ## External procedures
373
+
374
+ There are three types of external procedures: data procedures (DP), plotting procedures (PP), and labeling procedures (LP). They must be defined in files called `dp_<name>.py`, `pp_<name>.py`, and `lp_<name>.py`, respectively. Each module is only imported once for the lifetime of the application. Each module defines a single class that implements the corresponding protocol, with the name that's `<name>` converted to PascalCase (e.g., `dp_EstTV` → `EstTVDP`, `pp_movie` → `MoviePP`, `lp_find_gest` → `FindGestLP`). The class is instantiated once per variable window.
375
+
376
+ ### Data procedures
377
+
378
+ The class must implement the following protocol:
379
+
380
+ ```py
381
+ TODO
382
+ ```
383
+
384
+ - `dp_AggVel`
385
+ - `dp_AZEL`
386
+ - `dp_EstTV`
387
+ - `dp_F0`
388
+ - `dp_FAlabels`
389
+ - `dp_flip`
390
+ - `dp_formants`
391
+ - `dp_JawAngle`
392
+ - `dp_LipAperture`
393
+ - `dp_LP`
394
+ - `dp_PalDist`
395
+ - `dp_PCA`
396
+ - `dp_SubtractJaw`
397
+ - `dp_TongArea`
398
+ - `dp_TongVel`
399
+ - `dp_traj`
400
+ - `dp_vel`
401
+
402
+ ### Plotting procedures
403
+
404
+ The class must implement the following protocol:
405
+
406
+ ```py
407
+ TODO
408
+ ```
409
+
410
+ - `pp_movie`
411
+ - `pp_phase`
412
+
413
+ ### Labeling procedures
414
+
415
+ A labeling procedure controls all behaviors relevant to labeling, namely:
416
+
417
+ - Configure its own behaviors (either on first load or through [Label menu > Labeling behavior > Configure](#label-menu)).
418
+ - Plotting the labels in the temporal view.
419
+ - Handling right-clicks in the temporal view (create).
420
+ - Handling double-clicks on labels in the temporal view (edit/delete).
421
+ - Handling dragging of labels in the temporal view.
422
+ - [Label menu](#label-menu) actions: edit, export, import, save, load.
423
+
424
+ The class must implement the following protocol:
425
+
426
+ ```py
427
+ TODO
428
+ ```
429
+
430
+ Unlike MVIEW, the default labeling procedure is not special-cased; it is also defined in a file.
431
+
432
+ - `lp_default`
433
+ - `lp_exportvals`
434
+ - `lp_extents`
435
+ - `lp_findgest`
436
+ - `lp_peaks`
437
+ - `lp_phase_ang`
438
+ - `lp_snapex`
439
+
440
+ ## Loading/saving configuration
441
+
442
+ TODO
443
+
444
+ ## Programmatic invocation
445
+
446
+ Currently only the `phonoscape()` function is supported. It's not really designed as a utility library; you can find many better alternatives.
447
+
448
+ The parameters are exactly the same as the [command line arguments](#command-line-arguments) (that is to say, the CLI is a very thin wrapper around the function). Just translate `-` to `_` and remove the leading `--`. For example:
449
+
450
+ ```py
451
+ phonoscape("./test_data/S02_data.mat", "*", palate="S02_pal", temporal_display=["AUDIO_SPECT", "TDx", "vTDx", "TDz", "vTDz"])
452
+ ```
@@ -0,0 +1,74 @@
1
+ import sys
2
+ from pathlib import Path
3
+ from typing import Unpack
4
+ from PySide6.QtWidgets import QApplication
5
+ import matplotlib.pyplot as plt
6
+
7
+ from .data.parse import load_variables, normalize_args, import_proc_ctor, CmdArgs
8
+ from .window import VarWindow, WindowManager
9
+ from .lproc.protocol import LabelProcedure
10
+
11
+
12
+ def phonoscape(file: str, variables: str = "*", **kwargs: Unpack[CmdArgs]) -> None:
13
+ plt.style.use("dark_background")
14
+
15
+ path = Path(file)
16
+ data, other_data, dimensions = load_variables(
17
+ path, variables, comps=kwargs.get("comps")
18
+ )
19
+
20
+ if not data:
21
+ raise ValueError(f"No matching variables found for pattern {variables!r}")
22
+
23
+ app_config, temporal_disp_specs, colors = normalize_args(
24
+ path, kwargs, data, other_data, dimensions
25
+ )
26
+
27
+ selected_variable = next(iter(data.keys()))
28
+
29
+ head_s = (kwargs.get("head") or 0) / 1000
30
+ tail_s = (kwargs.get("tail") or data[selected_variable].duration_s * 1000) / 1000
31
+ tail_s = min(tail_s, data[selected_variable].duration_s)
32
+ if head_s < 0:
33
+ raise ValueError(
34
+ f"--head must be a non-negative number of milliseconds, but got {head_s * 1000}"
35
+ )
36
+ if tail_s < 0:
37
+ raise ValueError(
38
+ f"--tail must be a non-negative number of milliseconds, but got {tail_s * 1000}"
39
+ )
40
+ if head_s >= tail_s:
41
+ raise ValueError(
42
+ f"--head must be less than --tail, but got head={head_s * 1000} and tail={tail_s * 1000}"
43
+ )
44
+ elif tail_s - head_s < 0.025:
45
+ raise ValueError(
46
+ f"The duration of the selection (tail - head) must be at least 25 milliseconds, but got head={head_s * 1000} and tail={tail_s * 1000} ({(tail_s - head_s) * 1000:.1f} ms)"
47
+ )
48
+
49
+ lproc_ctor_path = kwargs.get("lproc") or "phonoscape.lproc.lp_default"
50
+ lproc_ctor: type[LabelProcedure] = import_proc_ctor(lproc_ctor_path, "lp")
51
+
52
+ app = QApplication.instance()
53
+ owns_app = app is None
54
+
55
+ if app is None:
56
+ app = QApplication(sys.argv)
57
+
58
+ window_manager = WindowManager(lproc_ctor=lproc_ctor)
59
+ window = VarWindow(
60
+ window_manager=window_manager,
61
+ selected_variable=selected_variable,
62
+ temporal_disp_specs=temporal_disp_specs,
63
+ lproc_ctor=lproc_ctor,
64
+ colors=colors,
65
+ app_config=app_config,
66
+ view=kwargs.get("view") or (0, -90, 0),
67
+ head_s=head_s,
68
+ tail_s=tail_s,
69
+ custom={},
70
+ )
71
+ window.show()
72
+
73
+ if owns_app:
74
+ sys.exit(app.exec())