revoxx 1.0.2__tar.gz → 1.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.
- {revoxx-1.0.2/revoxx.egg-info → revoxx-1.1.0}/PKG-INFO +13 -5
- {revoxx-1.0.2 → revoxx-1.1.0}/README.md +11 -3
- {revoxx-1.0.2 → revoxx-1.1.0}/pyproject.toml +2 -2
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/app.py +31 -6
- revoxx-1.1.0/revoxx/audio/editor.py +291 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/audio/player.py +265 -118
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/audio/queue_manager.py +61 -19
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/audio/recorder.py +57 -16
- revoxx-1.1.0/revoxx/audio/worker_state.py +20 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/constants.py +13 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/controllers/__init__.py +2 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/controllers/audio_controller.py +147 -8
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/controllers/display_controller.py +50 -2
- revoxx-1.1.0/revoxx/controllers/edit_controller.py +441 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/doc/USER_GUIDE.md +48 -2
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/resources/keyboard_shortcuts.txt +13 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/ui/dialogs/user_guide_dialog.py +3 -1
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/ui/spectrogram/controllers/__init__.py +4 -1
- revoxx-1.1.0/revoxx/ui/spectrogram/controllers/playback_controller.py +224 -0
- revoxx-1.1.0/revoxx/ui/spectrogram/controllers/selection_visualizer.py +339 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/ui/spectrogram/playback_handler.py +294 -109
- revoxx-1.1.0/revoxx/ui/spectrogram/selection_state.py +139 -0
- revoxx-1.1.0/revoxx/ui/spectrogram/view_context.py +91 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/ui/spectrogram/widget.py +478 -2
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/ui/window_base.py +1 -5
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/ui/window_factory.py +1 -2
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/utils/state.py +2 -2
- {revoxx-1.0.2 → revoxx-1.1.0/revoxx.egg-info}/PKG-INFO +13 -5
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx.egg-info/SOURCES.txt +6 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx.egg-info/requires.txt +1 -1
- {revoxx-1.0.2 → revoxx-1.1.0}/tests/test_audio_controller.py +9 -6
- {revoxx-1.0.2 → revoxx-1.1.0}/tests/test_audio_queue_manager.py +8 -2
- revoxx-1.0.2/revoxx/ui/spectrogram/controllers/playback_controller.py +0 -116
- {revoxx-1.0.2 → revoxx-1.1.0}/LICENSE +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/MANIFEST.in +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/doc/import_raw_text.png +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/doc/screenshot1.png +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/__init__.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/__main__.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/audio/__init__.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/audio/audio_buffer.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/audio/audio_queue_processor.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/audio/buffer_manager.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/audio/level_calculator.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/audio/processors/__init__.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/audio/processors/clipping_detector.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/audio/processors/mel_spectrogram.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/audio/processors/processor_base.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/audio/shared_state.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/controllers/device_controller.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/controllers/dialog_controller.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/controllers/file_operations_controller.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/controllers/navigation_controller.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/controllers/process_manager.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/controllers/session_controller.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/dataset/__init__.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/dataset/exporter.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/resources/microphone.png +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/resources/templates/dataset_readme.txt +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/resources/templates/index_format_with_intensity.txt +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/resources/templates/index_format_without_intensity.txt +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/session/__init__.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/session/inspector.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/session/manager.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/session/models.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/session/script_parser.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/ui/__init__.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/ui/dialogs/__init__.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/ui/dialogs/dataset_dialog.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/ui/dialogs/dialog_utils.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/ui/dialogs/find_dialog.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/ui/dialogs/help_dialog.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/ui/dialogs/import_text_dialog.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/ui/dialogs/new_session_dialog.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/ui/dialogs/open_session_dialog.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/ui/dialogs/progress_dialog.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/ui/dialogs/session_settings_dialog.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/ui/dialogs/utterance_list_base.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/ui/dialogs/utterance_order_dialog.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/ui/emotion_indicator.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/ui/font_manager.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/ui/frequency_axis.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/ui/icon.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/ui/info_overlay.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/ui/level_meter/__init__.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/ui/level_meter/config.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/ui/level_meter/led_level_meter.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/ui/menus/application_menu.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/ui/menus/audio_devices.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/ui/recording_display_state.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/ui/spectrogram/__init__.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/ui/spectrogram/controllers/clipping_visualizer.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/ui/spectrogram/controllers/edge_indicator.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/ui/spectrogram/controllers/zoom_controller.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/ui/spectrogram/display_base.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/ui/spectrogram/display_utils.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/ui/spectrogram/mel_processor_manager.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/ui/spectrogram/recording_display.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/ui/spectrogram/recording_handler.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/ui/style_config.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/ui/themes.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/ui/widget_initializer.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/ui/window_manager.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/utils/__init__.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/utils/active_recordings.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/utils/audio_utils.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/utils/config.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/utils/device_manager.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/utils/file_manager.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/utils/process_cleanup.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/utils/settings_manager.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/utils/spectrogram_utils.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/utils/text_importer.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx/utils/text_utils.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx.egg-info/dependency_links.txt +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx.egg-info/entry_points.txt +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/revoxx.egg-info/top_level.txt +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/scripts_module/__init__.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/scripts_module/export.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/scripts_module/vadiate.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/setup.cfg +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/tests/test_active_recordings.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/tests/test_config.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/tests/test_dataset_exporter.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/tests/test_device_controller.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/tests/test_dialog_controller.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/tests/test_display_controller.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/tests/test_file_manager.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/tests/test_file_operations_controller.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/tests/test_ipc_communication.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/tests/test_navigation_controller.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/tests/test_new_session_dialog.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/tests/test_process_manager.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/tests/test_session_controller.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/tests/test_session_manager.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/tests/test_session_models.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/tests/test_stable_sorting.py +0 -0
- {revoxx-1.0.2 → revoxx-1.1.0}/tests/test_utterance_list_dialog_sorting.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: revoxx
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Summary: Speech recording application for creating high-quality speech datasets
|
|
5
5
|
Author-email: Grammatek ehf <info@grammatek.com>
|
|
6
6
|
Maintainer-email: Grammatek ehf <info@grammatek.com>
|
|
@@ -38,7 +38,7 @@ Requires-Dist: torch>=2.0.0; extra == "vad"
|
|
|
38
38
|
Requires-Dist: silero-vad>=5.0; extra == "vad"
|
|
39
39
|
Requires-Dist: torchaudio<2.8.0; extra == "vad"
|
|
40
40
|
Provides-Extra: dev
|
|
41
|
-
Requires-Dist: black
|
|
41
|
+
Requires-Dist: black<26,>=25.0.0; extra == "dev"
|
|
42
42
|
Requires-Dist: isort>=5.10.0; extra == "dev"
|
|
43
43
|
Requires-Dist: flake8>=6.0.0; extra == "dev"
|
|
44
44
|
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
@@ -50,14 +50,14 @@ Dynamic: license-file
|
|
|
50
50
|
|
|
51
51
|
This repository provides **Revoxx**, a graphical recording application for recording raw speech and generating datasets.
|
|
52
52
|
|
|
53
|
-
](https://pypi.org/project/revoxx/)
|
|
54
54
|

|
|
55
55
|

|
|
56
56
|

|
|
57
57
|

|
|
58
58
|

|
|
59
59
|
[](https://github.com/icelandic-lt/revoxx/actions/workflows/build.yml)
|
|
60
|
-
](https://deepwiki.com/icelandic-lt/revoxx)
|
|
61
61
|
|
|
62
62
|
## Overview
|
|
63
63
|
|
|
@@ -114,6 +114,12 @@ the Icelandic emotional speech dataset, and created this tool to minimize hassle
|
|
|
114
114
|
- **Real-time monitoring** including toggable recording levels, mel spectrograms, maximum frequency detection, and more
|
|
115
115
|
- Customizable **industry-standard presets for Peak/RMS levels**
|
|
116
116
|
- Dedicated **Monitoring mode** for precise input calibration
|
|
117
|
+
- **Audio Editing** directly in the spectrogram view
|
|
118
|
+
- Set position markers to play from any point in the recording
|
|
119
|
+
- Create selection ranges for partial playback
|
|
120
|
+
- Delete ranges with automatic crossfade
|
|
121
|
+
- Insert new audio at marker position
|
|
122
|
+
- Replace selected ranges with new recordings
|
|
117
123
|
- **Multi-Screen Support**
|
|
118
124
|
- You can use multiple monitors to **separate recording view from speaker view**
|
|
119
125
|
- We support Apple's [Sidecar](https://support.apple.com/en-us/102597) feature for a **convenient dual screen setup with an external iPad**
|
|
@@ -244,12 +250,14 @@ pip install -e .[dev,vad]
|
|
|
244
250
|
```
|
|
245
251
|
|
|
246
252
|
Development dependencies include:
|
|
247
|
-
- **black**: Code formatter
|
|
253
|
+
- **black**: Code formatter (pinned to 25.x for Python 3.9 compatibility)
|
|
248
254
|
- **isort**: Import statement organizer
|
|
249
255
|
- **flake8**: Code linter
|
|
250
256
|
- **pytest**: Testing framework
|
|
251
257
|
- **pytest-cov**: Code coverage reporting
|
|
252
258
|
|
|
259
|
+
> **Note**: Black is pinned to version 25.x because Black 26+ requires Python 3.10+ and introduces the "2026 stable style" with different formatting rules. This ensures consistent formatting across all supported Python versions (3.9-3.13).
|
|
260
|
+
|
|
253
261
|
### Running code quality checks
|
|
254
262
|
|
|
255
263
|
```bash
|
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
This repository provides **Revoxx**, a graphical recording application for recording raw speech and generating datasets.
|
|
4
4
|
|
|
5
|
-
](https://pypi.org/project/revoxx/)
|
|
6
6
|

|
|
7
7
|

|
|
8
8
|

|
|
9
9
|

|
|
10
10
|

|
|
11
11
|
[](https://github.com/icelandic-lt/revoxx/actions/workflows/build.yml)
|
|
12
|
-
](https://deepwiki.com/icelandic-lt/revoxx)
|
|
13
13
|
|
|
14
14
|
## Overview
|
|
15
15
|
|
|
@@ -66,6 +66,12 @@ the Icelandic emotional speech dataset, and created this tool to minimize hassle
|
|
|
66
66
|
- **Real-time monitoring** including toggable recording levels, mel spectrograms, maximum frequency detection, and more
|
|
67
67
|
- Customizable **industry-standard presets for Peak/RMS levels**
|
|
68
68
|
- Dedicated **Monitoring mode** for precise input calibration
|
|
69
|
+
- **Audio Editing** directly in the spectrogram view
|
|
70
|
+
- Set position markers to play from any point in the recording
|
|
71
|
+
- Create selection ranges for partial playback
|
|
72
|
+
- Delete ranges with automatic crossfade
|
|
73
|
+
- Insert new audio at marker position
|
|
74
|
+
- Replace selected ranges with new recordings
|
|
69
75
|
- **Multi-Screen Support**
|
|
70
76
|
- You can use multiple monitors to **separate recording view from speaker view**
|
|
71
77
|
- We support Apple's [Sidecar](https://support.apple.com/en-us/102597) feature for a **convenient dual screen setup with an external iPad**
|
|
@@ -196,12 +202,14 @@ pip install -e .[dev,vad]
|
|
|
196
202
|
```
|
|
197
203
|
|
|
198
204
|
Development dependencies include:
|
|
199
|
-
- **black**: Code formatter
|
|
205
|
+
- **black**: Code formatter (pinned to 25.x for Python 3.9 compatibility)
|
|
200
206
|
- **isort**: Import statement organizer
|
|
201
207
|
- **flake8**: Code linter
|
|
202
208
|
- **pytest**: Testing framework
|
|
203
209
|
- **pytest-cov**: Code coverage reporting
|
|
204
210
|
|
|
211
|
+
> **Note**: Black is pinned to version 25.x because Black 26+ requires Python 3.10+ and introduces the "2026 stable style" with different formatting rules. This ensures consistent formatting across all supported Python versions (3.9-3.13).
|
|
212
|
+
|
|
205
213
|
### Running code quality checks
|
|
206
214
|
|
|
207
215
|
```bash
|
|
@@ -51,7 +51,7 @@ vad = [
|
|
|
51
51
|
# pip install torch --index-url https://download.pytorch.org/whl/cpu
|
|
52
52
|
# pip install revoxx[vad]
|
|
53
53
|
dev = [
|
|
54
|
-
"black>=
|
|
54
|
+
"black>=25.0.0,<26",
|
|
55
55
|
"isort>=5.10.0",
|
|
56
56
|
"flake8>=6.0.0",
|
|
57
57
|
"pytest>=7.0.0",
|
|
@@ -87,7 +87,7 @@ revoxx = [
|
|
|
87
87
|
|
|
88
88
|
[tool.black]
|
|
89
89
|
line-length = 88
|
|
90
|
-
target-version = ['py39', 'py310', 'py311']
|
|
90
|
+
target-version = ['py39', 'py310', 'py311', 'py312', 'py313']
|
|
91
91
|
include = '\.pyi?$'
|
|
92
92
|
extend-exclude = '''
|
|
93
93
|
(
|
|
@@ -11,7 +11,7 @@ from pathlib import Path
|
|
|
11
11
|
from typing import Optional
|
|
12
12
|
import traceback
|
|
13
13
|
|
|
14
|
-
from .constants import KeyBindings, FileConstants, MsgType
|
|
14
|
+
from .constants import KeyBindings, FileConstants, MsgType, UIConstants
|
|
15
15
|
from .utils.config import RecorderConfig, load_config
|
|
16
16
|
from .utils.state import AppState
|
|
17
17
|
from .utils.file_manager import RecordingFileManager, ScriptFileManager
|
|
@@ -29,6 +29,7 @@ from .session import SessionManager, Session
|
|
|
29
29
|
# Import all controllers
|
|
30
30
|
from .controllers import (
|
|
31
31
|
AudioController,
|
|
32
|
+
EditController,
|
|
32
33
|
NavigationController,
|
|
33
34
|
SessionController,
|
|
34
35
|
DeviceController,
|
|
@@ -168,8 +169,6 @@ class Revoxx:
|
|
|
168
169
|
theme_manager.set_theme(ThemePreset.CYAN)
|
|
169
170
|
|
|
170
171
|
# Refresh UI constants with theme colors
|
|
171
|
-
from .constants import UIConstants
|
|
172
|
-
|
|
173
172
|
UIConstants.refresh()
|
|
174
173
|
|
|
175
174
|
# Initialize WindowManager
|
|
@@ -239,6 +238,7 @@ class Revoxx:
|
|
|
239
238
|
self.display_controller = DisplayController(self, self.window_manager)
|
|
240
239
|
self.file_operations_controller = FileOperationsController(self)
|
|
241
240
|
self.dialog_controller = DialogController(self)
|
|
241
|
+
self.edit_controller = EditController(self)
|
|
242
242
|
|
|
243
243
|
def _populate_app_callbacks(self):
|
|
244
244
|
"""Populate app_callbacks dictionary with controller methods."""
|
|
@@ -308,13 +308,17 @@ class Revoxx:
|
|
|
308
308
|
self.window.window.bind(
|
|
309
309
|
f"<{KeyBindings.PLAY}>", lambda e: self.audio_controller.play_current()
|
|
310
310
|
)
|
|
311
|
+
self.window.window.bind(
|
|
312
|
+
f"<{KeyBindings.STOP}>",
|
|
313
|
+
lambda e: self.audio_controller.stop_all_playback_activities(),
|
|
314
|
+
)
|
|
311
315
|
self.window.window.bind(
|
|
312
316
|
"<Control-d>",
|
|
313
|
-
lambda e: self.
|
|
317
|
+
lambda e: self._handle_delete(),
|
|
314
318
|
)
|
|
315
319
|
self.window.window.bind(
|
|
316
320
|
"<Control-D>",
|
|
317
|
-
lambda e: self.
|
|
321
|
+
lambda e: self._handle_delete(),
|
|
318
322
|
)
|
|
319
323
|
|
|
320
324
|
# Navigation keys
|
|
@@ -379,7 +383,7 @@ class Revoxx:
|
|
|
379
383
|
modifier = "Control"
|
|
380
384
|
self.window.window.bind(
|
|
381
385
|
f"<{modifier}-{KeyBindings.DELETE_RECORDING}>",
|
|
382
|
-
lambda e: self.
|
|
386
|
+
lambda e: self._handle_delete(),
|
|
383
387
|
)
|
|
384
388
|
|
|
385
389
|
# Help and info
|
|
@@ -391,6 +395,11 @@ class Revoxx:
|
|
|
391
395
|
lambda e: self.display_controller.toggle_info_panel(),
|
|
392
396
|
)
|
|
393
397
|
|
|
398
|
+
# Clear selection with Escape
|
|
399
|
+
self.window.window.bind(
|
|
400
|
+
"<Escape>", lambda e: self._clear_spectrogram_selection()
|
|
401
|
+
)
|
|
402
|
+
|
|
394
403
|
# Second window shortcuts (Shift + key)
|
|
395
404
|
self.window.window.bind(
|
|
396
405
|
"<Shift-M>",
|
|
@@ -594,6 +603,22 @@ class Revoxx:
|
|
|
594
603
|
# Exit
|
|
595
604
|
sys.exit(0)
|
|
596
605
|
|
|
606
|
+
def _clear_spectrogram_selection(self) -> None:
|
|
607
|
+
"""Clear marker and selection in spectrogram."""
|
|
608
|
+
if self.window and self.window.mel_spectrogram:
|
|
609
|
+
self.window.mel_spectrogram.clear_selection()
|
|
610
|
+
|
|
611
|
+
def _handle_delete(self) -> None:
|
|
612
|
+
"""Handle delete action - deletes selection if active, otherwise recording."""
|
|
613
|
+
if (
|
|
614
|
+
self.window
|
|
615
|
+
and self.window.mel_spectrogram
|
|
616
|
+
and self.window.mel_spectrogram.selection_state.has_selection
|
|
617
|
+
):
|
|
618
|
+
self.edit_controller.delete_selection()
|
|
619
|
+
else:
|
|
620
|
+
self.file_operations_controller.delete_current_recording()
|
|
621
|
+
|
|
597
622
|
def _toggle_fullscreen(self):
|
|
598
623
|
"""Toggle fullscreen mode.
|
|
599
624
|
This setting is saved to the user's settings.
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"""Audio editing operations with cross-fade support.
|
|
2
|
+
|
|
3
|
+
This module provides audio editing functions for deleting, inserting,
|
|
4
|
+
and replacing audio segments with smooth cross-fade transitions.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from ..constants import AudioConstants
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AudioEditor:
|
|
13
|
+
"""Provides audio editing operations with cross-fade support.
|
|
14
|
+
|
|
15
|
+
All operations use equal-power cross-fading to ensure smooth
|
|
16
|
+
transitions without audible clicks or pops.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
@staticmethod
|
|
20
|
+
def delete_range(
|
|
21
|
+
audio: np.ndarray, start_sample: int, end_sample: int, sample_rate: int
|
|
22
|
+
) -> np.ndarray:
|
|
23
|
+
"""Delete a range of audio samples with cross-fade.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
audio: Input audio array
|
|
27
|
+
start_sample: Start of deletion range
|
|
28
|
+
end_sample: End of deletion range
|
|
29
|
+
sample_rate: Audio sample rate in Hz
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
New audio array with the range deleted and cross-faded
|
|
33
|
+
"""
|
|
34
|
+
if start_sample >= end_sample:
|
|
35
|
+
return audio.copy()
|
|
36
|
+
|
|
37
|
+
if start_sample < 0:
|
|
38
|
+
start_sample = 0
|
|
39
|
+
if end_sample > len(audio):
|
|
40
|
+
end_sample = len(audio)
|
|
41
|
+
|
|
42
|
+
# Calculate fade samples
|
|
43
|
+
selection_samples = end_sample - start_sample
|
|
44
|
+
fade_samples = AudioEditor._calculate_fade_samples(
|
|
45
|
+
sample_rate, selection_samples
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Get the parts before and after the deletion
|
|
49
|
+
before = audio[:start_sample]
|
|
50
|
+
after = audio[end_sample:]
|
|
51
|
+
|
|
52
|
+
if (
|
|
53
|
+
fade_samples > 0
|
|
54
|
+
and len(before) >= fade_samples
|
|
55
|
+
and len(after) >= fade_samples
|
|
56
|
+
):
|
|
57
|
+
# Apply cross-fade between the end of 'before' and start of 'after'
|
|
58
|
+
before_fade = before[-fade_samples:]
|
|
59
|
+
after_fade = after[:fade_samples]
|
|
60
|
+
|
|
61
|
+
crossfaded = AudioEditor._equal_power_crossfade(
|
|
62
|
+
before_fade, after_fade, fade_samples
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Construct result: before (minus fade region) + crossfade + after (minus fade region)
|
|
66
|
+
result = np.concatenate(
|
|
67
|
+
[before[:-fade_samples], crossfaded, after[fade_samples:]]
|
|
68
|
+
)
|
|
69
|
+
else:
|
|
70
|
+
# No cross-fade possible, just concatenate
|
|
71
|
+
result = np.concatenate([before, after])
|
|
72
|
+
|
|
73
|
+
return result
|
|
74
|
+
|
|
75
|
+
@staticmethod
|
|
76
|
+
def insert_at_position(
|
|
77
|
+
original: np.ndarray,
|
|
78
|
+
insert: np.ndarray,
|
|
79
|
+
position: int,
|
|
80
|
+
sample_rate: int,
|
|
81
|
+
) -> np.ndarray:
|
|
82
|
+
"""Insert audio at a position with cross-fade.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
original: Original audio array
|
|
86
|
+
insert: Audio to insert
|
|
87
|
+
position: Sample position to insert at
|
|
88
|
+
sample_rate: Audio sample rate in Hz
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
New audio array with the inserted content cross-faded
|
|
92
|
+
"""
|
|
93
|
+
if len(insert) == 0:
|
|
94
|
+
return original.copy()
|
|
95
|
+
|
|
96
|
+
if position < 0:
|
|
97
|
+
position = 0
|
|
98
|
+
if position > len(original):
|
|
99
|
+
position = len(original)
|
|
100
|
+
|
|
101
|
+
# Calculate fade samples based on insert length
|
|
102
|
+
fade_samples = AudioEditor._calculate_fade_samples(sample_rate, len(insert))
|
|
103
|
+
|
|
104
|
+
before = original[:position]
|
|
105
|
+
after = original[position:]
|
|
106
|
+
|
|
107
|
+
if fade_samples > 0:
|
|
108
|
+
# Cross-fade at insertion point
|
|
109
|
+
result_parts = []
|
|
110
|
+
|
|
111
|
+
# Before section
|
|
112
|
+
if len(before) >= fade_samples:
|
|
113
|
+
# Fade out the end of 'before' and fade in start of 'insert'
|
|
114
|
+
before_fade = before[-fade_samples:]
|
|
115
|
+
insert_start_fade = (
|
|
116
|
+
insert[:fade_samples] if len(insert) >= fade_samples else insert
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
if len(insert_start_fade) == fade_samples:
|
|
120
|
+
crossfaded_start = AudioEditor._equal_power_crossfade(
|
|
121
|
+
before_fade, insert_start_fade, fade_samples
|
|
122
|
+
)
|
|
123
|
+
result_parts.append(before[:-fade_samples])
|
|
124
|
+
result_parts.append(crossfaded_start)
|
|
125
|
+
else:
|
|
126
|
+
result_parts.append(before)
|
|
127
|
+
else:
|
|
128
|
+
result_parts.append(before)
|
|
129
|
+
|
|
130
|
+
# Middle section of insert (if any)
|
|
131
|
+
if len(insert) > 2 * fade_samples:
|
|
132
|
+
result_parts.append(insert[fade_samples:-fade_samples])
|
|
133
|
+
elif len(insert) > fade_samples:
|
|
134
|
+
result_parts.append(insert[fade_samples:])
|
|
135
|
+
|
|
136
|
+
# After section
|
|
137
|
+
if len(after) >= fade_samples and len(insert) >= fade_samples:
|
|
138
|
+
# Fade out end of 'insert' and fade in start of 'after'
|
|
139
|
+
insert_end_fade = insert[-fade_samples:]
|
|
140
|
+
after_fade = after[:fade_samples]
|
|
141
|
+
|
|
142
|
+
crossfaded_end = AudioEditor._equal_power_crossfade(
|
|
143
|
+
insert_end_fade, after_fade, fade_samples
|
|
144
|
+
)
|
|
145
|
+
result_parts.append(crossfaded_end)
|
|
146
|
+
result_parts.append(after[fade_samples:])
|
|
147
|
+
else:
|
|
148
|
+
result_parts.append(after)
|
|
149
|
+
|
|
150
|
+
result = np.concatenate([p for p in result_parts if len(p) > 0])
|
|
151
|
+
else:
|
|
152
|
+
# No cross-fade, simple concatenation
|
|
153
|
+
result = np.concatenate([before, insert, after])
|
|
154
|
+
|
|
155
|
+
return result
|
|
156
|
+
|
|
157
|
+
@staticmethod
|
|
158
|
+
def replace_range(
|
|
159
|
+
original: np.ndarray,
|
|
160
|
+
replacement: np.ndarray,
|
|
161
|
+
start_sample: int,
|
|
162
|
+
end_sample: int,
|
|
163
|
+
sample_rate: int,
|
|
164
|
+
) -> np.ndarray:
|
|
165
|
+
"""Replace a range of audio with new content and cross-fade.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
original: Original audio array
|
|
169
|
+
replacement: Audio to replace the range with
|
|
170
|
+
start_sample: Start of range to replace
|
|
171
|
+
end_sample: End of range to replace
|
|
172
|
+
sample_rate: Audio sample rate in Hz
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
New audio array with the range replaced and cross-faded
|
|
176
|
+
"""
|
|
177
|
+
if start_sample >= end_sample:
|
|
178
|
+
return original.copy()
|
|
179
|
+
|
|
180
|
+
if start_sample < 0:
|
|
181
|
+
start_sample = 0
|
|
182
|
+
if end_sample > len(original):
|
|
183
|
+
end_sample = len(original)
|
|
184
|
+
|
|
185
|
+
# Calculate fade samples
|
|
186
|
+
selection_samples = end_sample - start_sample
|
|
187
|
+
fade_samples = AudioEditor._calculate_fade_samples(
|
|
188
|
+
sample_rate, min(selection_samples, len(replacement))
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
before = original[:start_sample]
|
|
192
|
+
after = original[end_sample:]
|
|
193
|
+
|
|
194
|
+
if fade_samples > 0:
|
|
195
|
+
result_parts = []
|
|
196
|
+
|
|
197
|
+
# Cross-fade at start of replacement
|
|
198
|
+
if len(before) >= fade_samples and len(replacement) >= fade_samples:
|
|
199
|
+
before_fade = before[-fade_samples:]
|
|
200
|
+
replacement_start = replacement[:fade_samples]
|
|
201
|
+
|
|
202
|
+
crossfaded_start = AudioEditor._equal_power_crossfade(
|
|
203
|
+
before_fade, replacement_start, fade_samples
|
|
204
|
+
)
|
|
205
|
+
result_parts.append(before[:-fade_samples])
|
|
206
|
+
result_parts.append(crossfaded_start)
|
|
207
|
+
else:
|
|
208
|
+
result_parts.append(before)
|
|
209
|
+
|
|
210
|
+
# Middle of replacement
|
|
211
|
+
if len(replacement) > 2 * fade_samples:
|
|
212
|
+
result_parts.append(replacement[fade_samples:-fade_samples])
|
|
213
|
+
elif len(replacement) > fade_samples:
|
|
214
|
+
result_parts.append(replacement[fade_samples:])
|
|
215
|
+
|
|
216
|
+
# Cross-fade at end of replacement
|
|
217
|
+
if len(after) >= fade_samples and len(replacement) >= fade_samples:
|
|
218
|
+
replacement_end = replacement[-fade_samples:]
|
|
219
|
+
after_start = after[:fade_samples]
|
|
220
|
+
|
|
221
|
+
crossfaded_end = AudioEditor._equal_power_crossfade(
|
|
222
|
+
replacement_end, after_start, fade_samples
|
|
223
|
+
)
|
|
224
|
+
result_parts.append(crossfaded_end)
|
|
225
|
+
result_parts.append(after[fade_samples:])
|
|
226
|
+
else:
|
|
227
|
+
result_parts.append(after)
|
|
228
|
+
|
|
229
|
+
result = np.concatenate([p for p in result_parts if len(p) > 0])
|
|
230
|
+
else:
|
|
231
|
+
# No cross-fade, simple replacement
|
|
232
|
+
result = np.concatenate([before, replacement, after])
|
|
233
|
+
|
|
234
|
+
return result
|
|
235
|
+
|
|
236
|
+
@staticmethod
|
|
237
|
+
def _equal_power_crossfade(
|
|
238
|
+
audio_a: np.ndarray, audio_b: np.ndarray, fade_samples: int
|
|
239
|
+
) -> np.ndarray:
|
|
240
|
+
"""Apply equal-power cross-fade between two audio segments.
|
|
241
|
+
|
|
242
|
+
Equal-power cross-fade maintains constant perceived loudness
|
|
243
|
+
during the transition by using sine/cosine curves for gain.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
audio_a: First audio segment (fade out)
|
|
247
|
+
audio_b: Second audio segment (fade in)
|
|
248
|
+
fade_samples: Number of samples for the fade
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
Cross-faded audio segment of length fade_samples
|
|
252
|
+
"""
|
|
253
|
+
if fade_samples <= 0:
|
|
254
|
+
return audio_b[:0] if len(audio_b) > 0 else np.array([])
|
|
255
|
+
|
|
256
|
+
# Ensure we have enough samples
|
|
257
|
+
actual_samples = min(fade_samples, len(audio_a), len(audio_b))
|
|
258
|
+
if actual_samples <= 0:
|
|
259
|
+
return np.array([])
|
|
260
|
+
|
|
261
|
+
# Equal power cross-fade using sine/cosine curves
|
|
262
|
+
t = np.linspace(0, np.pi / 2, actual_samples)
|
|
263
|
+
gain_a = np.cos(t) # Fade out
|
|
264
|
+
gain_b = np.sin(t) # Fade in
|
|
265
|
+
|
|
266
|
+
result = audio_a[:actual_samples] * gain_a + audio_b[:actual_samples] * gain_b
|
|
267
|
+
|
|
268
|
+
return result
|
|
269
|
+
|
|
270
|
+
@staticmethod
|
|
271
|
+
def _calculate_fade_samples(sample_rate: int, selection_samples: int) -> int:
|
|
272
|
+
"""Calculate the number of samples for cross-fade.
|
|
273
|
+
|
|
274
|
+
The fade length is adaptive: it uses the configured cross-fade duration
|
|
275
|
+
but is capped at half the selection length to ensure smooth transitions
|
|
276
|
+
for short selections.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
sample_rate: Audio sample rate in Hz
|
|
280
|
+
selection_samples: Number of samples in the selection
|
|
281
|
+
|
|
282
|
+
Returns:
|
|
283
|
+
Number of samples to use for cross-fade
|
|
284
|
+
"""
|
|
285
|
+
# Calculate fade samples from configured duration
|
|
286
|
+
fade_from_config = int(AudioConstants.CROSSFADE_MS * sample_rate / 1000)
|
|
287
|
+
|
|
288
|
+
# Cap at half the selection length
|
|
289
|
+
max_fade = selection_samples // 2
|
|
290
|
+
|
|
291
|
+
return min(fade_from_config, max_fade)
|