revoxx 1.0.1__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.
Files changed (138) hide show
  1. {revoxx-1.0.1/revoxx.egg-info → revoxx-1.1.0}/PKG-INFO +14 -6
  2. {revoxx-1.0.1 → revoxx-1.1.0}/README.md +12 -4
  3. {revoxx-1.0.1 → revoxx-1.1.0}/pyproject.toml +4 -3
  4. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/app.py +31 -6
  5. revoxx-1.1.0/revoxx/audio/editor.py +291 -0
  6. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/audio/player.py +265 -118
  7. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/audio/queue_manager.py +61 -19
  8. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/audio/recorder.py +57 -16
  9. revoxx-1.1.0/revoxx/audio/worker_state.py +20 -0
  10. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/constants.py +13 -0
  11. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/controllers/__init__.py +2 -0
  12. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/controllers/audio_controller.py +147 -8
  13. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/controllers/display_controller.py +50 -2
  14. revoxx-1.1.0/revoxx/controllers/edit_controller.py +441 -0
  15. {revoxx-1.0.1 → revoxx-1.1.0/revoxx}/doc/USER_GUIDE.md +48 -2
  16. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/resources/keyboard_shortcuts.txt +13 -0
  17. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/ui/dialogs/user_guide_dialog.py +7 -16
  18. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/ui/spectrogram/controllers/__init__.py +4 -1
  19. revoxx-1.1.0/revoxx/ui/spectrogram/controllers/playback_controller.py +224 -0
  20. revoxx-1.1.0/revoxx/ui/spectrogram/controllers/selection_visualizer.py +339 -0
  21. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/ui/spectrogram/playback_handler.py +294 -109
  22. revoxx-1.1.0/revoxx/ui/spectrogram/selection_state.py +139 -0
  23. revoxx-1.1.0/revoxx/ui/spectrogram/view_context.py +91 -0
  24. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/ui/spectrogram/widget.py +478 -2
  25. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/ui/window_base.py +1 -5
  26. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/ui/window_factory.py +1 -2
  27. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/utils/state.py +2 -2
  28. {revoxx-1.0.1 → revoxx-1.1.0/revoxx.egg-info}/PKG-INFO +14 -6
  29. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx.egg-info/SOURCES.txt +7 -1
  30. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx.egg-info/requires.txt +1 -1
  31. {revoxx-1.0.1 → revoxx-1.1.0}/tests/test_audio_controller.py +9 -6
  32. {revoxx-1.0.1 → revoxx-1.1.0}/tests/test_audio_queue_manager.py +8 -2
  33. revoxx-1.0.1/revoxx/ui/spectrogram/controllers/playback_controller.py +0 -116
  34. {revoxx-1.0.1 → revoxx-1.1.0}/LICENSE +0 -0
  35. {revoxx-1.0.1 → revoxx-1.1.0}/MANIFEST.in +0 -0
  36. {revoxx-1.0.1 → revoxx-1.1.0}/doc/import_raw_text.png +0 -0
  37. {revoxx-1.0.1 → revoxx-1.1.0}/doc/screenshot1.png +0 -0
  38. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/__init__.py +0 -0
  39. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/__main__.py +0 -0
  40. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/audio/__init__.py +0 -0
  41. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/audio/audio_buffer.py +0 -0
  42. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/audio/audio_queue_processor.py +0 -0
  43. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/audio/buffer_manager.py +0 -0
  44. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/audio/level_calculator.py +0 -0
  45. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/audio/processors/__init__.py +0 -0
  46. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/audio/processors/clipping_detector.py +0 -0
  47. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/audio/processors/mel_spectrogram.py +0 -0
  48. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/audio/processors/processor_base.py +0 -0
  49. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/audio/shared_state.py +0 -0
  50. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/controllers/device_controller.py +0 -0
  51. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/controllers/dialog_controller.py +0 -0
  52. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/controllers/file_operations_controller.py +0 -0
  53. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/controllers/navigation_controller.py +0 -0
  54. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/controllers/process_manager.py +0 -0
  55. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/controllers/session_controller.py +0 -0
  56. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/dataset/__init__.py +0 -0
  57. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/dataset/exporter.py +0 -0
  58. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/resources/microphone.png +0 -0
  59. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/resources/templates/dataset_readme.txt +0 -0
  60. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/resources/templates/index_format_with_intensity.txt +0 -0
  61. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/resources/templates/index_format_without_intensity.txt +0 -0
  62. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/session/__init__.py +0 -0
  63. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/session/inspector.py +0 -0
  64. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/session/manager.py +0 -0
  65. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/session/models.py +0 -0
  66. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/session/script_parser.py +0 -0
  67. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/ui/__init__.py +0 -0
  68. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/ui/dialogs/__init__.py +0 -0
  69. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/ui/dialogs/dataset_dialog.py +0 -0
  70. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/ui/dialogs/dialog_utils.py +0 -0
  71. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/ui/dialogs/find_dialog.py +0 -0
  72. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/ui/dialogs/help_dialog.py +0 -0
  73. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/ui/dialogs/import_text_dialog.py +0 -0
  74. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/ui/dialogs/new_session_dialog.py +0 -0
  75. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/ui/dialogs/open_session_dialog.py +0 -0
  76. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/ui/dialogs/progress_dialog.py +0 -0
  77. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/ui/dialogs/session_settings_dialog.py +0 -0
  78. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/ui/dialogs/utterance_list_base.py +0 -0
  79. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/ui/dialogs/utterance_order_dialog.py +0 -0
  80. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/ui/emotion_indicator.py +0 -0
  81. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/ui/font_manager.py +0 -0
  82. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/ui/frequency_axis.py +0 -0
  83. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/ui/icon.py +0 -0
  84. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/ui/info_overlay.py +0 -0
  85. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/ui/level_meter/__init__.py +0 -0
  86. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/ui/level_meter/config.py +0 -0
  87. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/ui/level_meter/led_level_meter.py +0 -0
  88. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/ui/menus/application_menu.py +0 -0
  89. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/ui/menus/audio_devices.py +0 -0
  90. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/ui/recording_display_state.py +0 -0
  91. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/ui/spectrogram/__init__.py +0 -0
  92. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/ui/spectrogram/controllers/clipping_visualizer.py +0 -0
  93. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/ui/spectrogram/controllers/edge_indicator.py +0 -0
  94. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/ui/spectrogram/controllers/zoom_controller.py +0 -0
  95. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/ui/spectrogram/display_base.py +0 -0
  96. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/ui/spectrogram/display_utils.py +0 -0
  97. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/ui/spectrogram/mel_processor_manager.py +0 -0
  98. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/ui/spectrogram/recording_display.py +0 -0
  99. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/ui/spectrogram/recording_handler.py +0 -0
  100. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/ui/style_config.py +0 -0
  101. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/ui/themes.py +0 -0
  102. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/ui/widget_initializer.py +0 -0
  103. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/ui/window_manager.py +0 -0
  104. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/utils/__init__.py +0 -0
  105. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/utils/active_recordings.py +0 -0
  106. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/utils/audio_utils.py +0 -0
  107. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/utils/config.py +0 -0
  108. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/utils/device_manager.py +0 -0
  109. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/utils/file_manager.py +0 -0
  110. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/utils/process_cleanup.py +0 -0
  111. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/utils/settings_manager.py +0 -0
  112. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/utils/spectrogram_utils.py +0 -0
  113. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/utils/text_importer.py +0 -0
  114. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx/utils/text_utils.py +0 -0
  115. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx.egg-info/dependency_links.txt +0 -0
  116. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx.egg-info/entry_points.txt +0 -0
  117. {revoxx-1.0.1 → revoxx-1.1.0}/revoxx.egg-info/top_level.txt +0 -0
  118. {revoxx-1.0.1 → revoxx-1.1.0}/scripts_module/__init__.py +0 -0
  119. {revoxx-1.0.1 → revoxx-1.1.0}/scripts_module/export.py +0 -0
  120. {revoxx-1.0.1 → revoxx-1.1.0}/scripts_module/vadiate.py +0 -0
  121. {revoxx-1.0.1 → revoxx-1.1.0}/setup.cfg +0 -0
  122. {revoxx-1.0.1 → revoxx-1.1.0}/tests/test_active_recordings.py +0 -0
  123. {revoxx-1.0.1 → revoxx-1.1.0}/tests/test_config.py +0 -0
  124. {revoxx-1.0.1 → revoxx-1.1.0}/tests/test_dataset_exporter.py +0 -0
  125. {revoxx-1.0.1 → revoxx-1.1.0}/tests/test_device_controller.py +0 -0
  126. {revoxx-1.0.1 → revoxx-1.1.0}/tests/test_dialog_controller.py +0 -0
  127. {revoxx-1.0.1 → revoxx-1.1.0}/tests/test_display_controller.py +0 -0
  128. {revoxx-1.0.1 → revoxx-1.1.0}/tests/test_file_manager.py +0 -0
  129. {revoxx-1.0.1 → revoxx-1.1.0}/tests/test_file_operations_controller.py +0 -0
  130. {revoxx-1.0.1 → revoxx-1.1.0}/tests/test_ipc_communication.py +0 -0
  131. {revoxx-1.0.1 → revoxx-1.1.0}/tests/test_navigation_controller.py +0 -0
  132. {revoxx-1.0.1 → revoxx-1.1.0}/tests/test_new_session_dialog.py +0 -0
  133. {revoxx-1.0.1 → revoxx-1.1.0}/tests/test_process_manager.py +0 -0
  134. {revoxx-1.0.1 → revoxx-1.1.0}/tests/test_session_controller.py +0 -0
  135. {revoxx-1.0.1 → revoxx-1.1.0}/tests/test_session_manager.py +0 -0
  136. {revoxx-1.0.1 → revoxx-1.1.0}/tests/test_session_models.py +0 -0
  137. {revoxx-1.0.1 → revoxx-1.1.0}/tests/test_stable_sorting.py +0 -0
  138. {revoxx-1.0.1 → 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.1
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>=22.0.0; extra == "dev"
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
- ![Version](https://img.shields.io/badge/Version-main-darkgreen)
53
+ [![PyPI version](https://img.shields.io/pypi/v/revoxx)](https://pypi.org/project/revoxx/)
54
54
  ![Python](https://img.shields.io/badge/python-3.9-blue?logo=python&logoColor=white)
55
55
  ![Python](https://img.shields.io/badge/python-3.10-blue?logo=python&logoColor=white)
56
56
  ![Python](https://img.shields.io/badge/python-3.11-blue?logo=python&logoColor=white)
57
57
  ![Python](https://img.shields.io/badge/python-3.12-blue?logo=python&logoColor=white)
58
58
  ![Python](https://img.shields.io/badge/python-3.13-blue?logo=python&logoColor=white)
59
59
  [![CI Status](https://github.com/icelandic-lt/revoxx/actions/workflows/build.yml/badge.svg)](https://github.com/icelandic-lt/revoxx/actions/workflows/build.yml)
60
- ![Docker](https://img.shields.io/badge/Docker-[unavailable]-red)
60
+ [![Ask DeepWiki](https://deepwiki.com/badge.svg)](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
@@ -315,7 +323,7 @@ revoxx --session path/to/session # Open specific session
315
323
 
316
324
  ## Usage
317
325
 
318
- For a guide on using Revoxx, please see the [User Guide](https://github.com/icelandic-lt/revoxx/blob/main/doc/USER_GUIDE.md).
326
+ For a guide on using Revoxx, please see the [User Guide](https://github.com/icelandic-lt/revoxx/blob/main/revoxx/doc/USER_GUIDE.md).
319
327
 
320
328
  ## Prepare recordings
321
329
 
@@ -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
- ![Version](https://img.shields.io/badge/Version-main-darkgreen)
5
+ [![PyPI version](https://img.shields.io/pypi/v/revoxx)](https://pypi.org/project/revoxx/)
6
6
  ![Python](https://img.shields.io/badge/python-3.9-blue?logo=python&logoColor=white)
7
7
  ![Python](https://img.shields.io/badge/python-3.10-blue?logo=python&logoColor=white)
8
8
  ![Python](https://img.shields.io/badge/python-3.11-blue?logo=python&logoColor=white)
9
9
  ![Python](https://img.shields.io/badge/python-3.12-blue?logo=python&logoColor=white)
10
10
  ![Python](https://img.shields.io/badge/python-3.13-blue?logo=python&logoColor=white)
11
11
  [![CI Status](https://github.com/icelandic-lt/revoxx/actions/workflows/build.yml/badge.svg)](https://github.com/icelandic-lt/revoxx/actions/workflows/build.yml)
12
- ![Docker](https://img.shields.io/badge/Docker-[unavailable]-red)
12
+ [![Ask DeepWiki](https://deepwiki.com/badge.svg)](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
@@ -267,7 +275,7 @@ revoxx --session path/to/session # Open specific session
267
275
 
268
276
  ## Usage
269
277
 
270
- For a guide on using Revoxx, please see the [User Guide](https://github.com/icelandic-lt/revoxx/blob/main/doc/USER_GUIDE.md).
278
+ For a guide on using Revoxx, please see the [User Guide](https://github.com/icelandic-lt/revoxx/blob/main/revoxx/doc/USER_GUIDE.md).
271
279
 
272
280
  ## Prepare recordings
273
281
 
@@ -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>=22.0.0",
54
+ "black>=25.0.0,<26",
55
55
  "isort>=5.10.0",
56
56
  "flake8>=6.0.0",
57
57
  "pytest>=7.0.0",
@@ -72,7 +72,7 @@ revoxx-vadiate = "scripts_module.vadiate:main"
72
72
 
73
73
  [tool.setuptools]
74
74
  packages = ["revoxx", "revoxx.audio", "revoxx.audio.processors", "revoxx.controllers",
75
- "revoxx.dataset", "revoxx.resources", "revoxx.resources.templates",
75
+ "revoxx.dataset", "revoxx.doc", "revoxx.resources", "revoxx.resources.templates",
76
76
  "revoxx.session", "revoxx.ui", "revoxx.ui.dialogs", "revoxx.ui.level_meter",
77
77
  "revoxx.ui.menus", "revoxx.ui.spectrogram", "revoxx.ui.spectrogram.controllers",
78
78
  "revoxx.utils", "scripts_module"]
@@ -82,11 +82,12 @@ include-package-data = true
82
82
  revoxx = [
83
83
  "resources/*.png",
84
84
  "resources/templates/*.txt",
85
+ "doc/*.md",
85
86
  ]
86
87
 
87
88
  [tool.black]
88
89
  line-length = 88
89
- target-version = ['py39', 'py310', 'py311']
90
+ target-version = ['py39', 'py310', 'py311', 'py312', 'py313']
90
91
  include = '\.pyi?$'
91
92
  extend-exclude = '''
92
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.file_operations_controller.delete_current_recording(),
317
+ lambda e: self._handle_delete(),
314
318
  )
315
319
  self.window.window.bind(
316
320
  "<Control-D>",
317
- lambda e: self.file_operations_controller.delete_current_recording(),
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.file_operations_controller.delete_current_recording(),
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)