musecbox 0.0.0__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. musecbox/__init__.py +383 -0
  2. musecbox/__main__.py +132 -0
  3. musecbox/audio_recorder.py +63 -0
  4. musecbox/dialogs/__init__ +0 -0
  5. musecbox/dialogs/add_group_dialog.py +264 -0
  6. musecbox/dialogs/add_group_dialog.ui +171 -0
  7. musecbox/dialogs/connection_dialog.py +459 -0
  8. musecbox/dialogs/connection_dialog.ui +235 -0
  9. musecbox/dialogs/copy_sfz_dialog.py +152 -0
  10. musecbox/dialogs/copy_sfz_paths_dialog.py +73 -0
  11. musecbox/dialogs/copy_sfzs_dialog.py +119 -0
  12. musecbox/dialogs/copy_sfzs_dialog.ui +146 -0
  13. musecbox/dialogs/generic_plugin_dialog.py +266 -0
  14. musecbox/dialogs/instrument_selection_dialog.py +288 -0
  15. musecbox/dialogs/instrument_selection_dialog.ui +486 -0
  16. musecbox/dialogs/missing_sfzs_dialog.py +161 -0
  17. musecbox/dialogs/project_info_dialog.py +103 -0
  18. musecbox/dialogs/project_info_dialog.ui +107 -0
  19. musecbox/dialogs/project_load_dialog.py +161 -0
  20. musecbox/dialogs/project_load_dialog.ui +99 -0
  21. musecbox/dialogs/project_save_dialog.py +164 -0
  22. musecbox/dialogs/record_dialog.py +120 -0
  23. musecbox/dialogs/record_dialog.ui +141 -0
  24. musecbox/dialogs/score_apply_dialog.py +162 -0
  25. musecbox/dialogs/score_apply_dialog.ui +171 -0
  26. musecbox/dialogs/score_import_channel_widget.ui +196 -0
  27. musecbox/dialogs/score_import_dialog.py +759 -0
  28. musecbox/dialogs/score_import_dialog.ui +236 -0
  29. musecbox/dialogs/score_import_part_widget.ui +167 -0
  30. musecbox/dialogs/score_info_dialog.py +97 -0
  31. musecbox/dialogs/score_info_dialog.ui +117 -0
  32. musecbox/dialogs/score_load_dialog.py +114 -0
  33. musecbox/dialogs/sfz_file_dialog.py +411 -0
  34. musecbox/dialogs/sfz_file_dialog.ui +284 -0
  35. musecbox/dialogs/sfzdb_dialog.py +289 -0
  36. musecbox/dialogs/sfzdb_dialog.ui +250 -0
  37. musecbox/dialogs/track_creation_dialog.py +87 -0
  38. musecbox/dialogs/track_creation_dialog.ui +190 -0
  39. musecbox/gui/__init__.py +37 -0
  40. musecbox/gui/balance_control_widget.py +627 -0
  41. musecbox/gui/horizontal_port_widget.ui +130 -0
  42. musecbox/gui/horizontal_track_plugin_widget.ui +273 -0
  43. musecbox/gui/horizontal_track_widget.ui +207 -0
  44. musecbox/gui/main_window.py +1508 -0
  45. musecbox/gui/main_window.ui +876 -0
  46. musecbox/gui/plugin_widgets.py +861 -0
  47. musecbox/gui/port_widget.py +528 -0
  48. musecbox/gui/shared_plugin_widget.ui +290 -0
  49. musecbox/gui/track_widget.py +783 -0
  50. musecbox/gui/vertical_port_widget.ui +138 -0
  51. musecbox/gui/vertical_track_plugin_widget.ui +273 -0
  52. musecbox/gui/vertical_track_widget.ui +240 -0
  53. musecbox/liquidsfz.py +68 -0
  54. musecbox/res/Theremin/Stereo-Theremin.sfz +77 -0
  55. musecbox/res/Theremin/Thick-Theremin.sfz +40 -0
  56. musecbox/res/Theremin/Thin-Theremin.sfz +43 -0
  57. musecbox/res/Theremin/samples/SINUS.wav +0 -0
  58. musecbox/res/Theremin/samples/U-sinewave.wav +0 -0
  59. musecbox/res/audio-icon-off.png +0 -0
  60. musecbox/res/audio-icon-on.png +0 -0
  61. musecbox/res/balance_control_widget.h +13 -0
  62. musecbox/res/collapse-vertical.svg +34 -0
  63. musecbox/res/collapse.svg +35 -0
  64. musecbox/res/control-icon-off.png +0 -0
  65. musecbox/res/control-icon-on.png +0 -0
  66. musecbox/res/disable-off.png +0 -0
  67. musecbox/res/disable-on.png +0 -0
  68. musecbox/res/empty.sfz +4 -0
  69. musecbox/res/expand-vertical.svg +34 -0
  70. musecbox/res/expand.svg +38 -0
  71. musecbox/res/lock.svg +3 -0
  72. musecbox/res/menu.svg +8 -0
  73. musecbox/res/meter.png +0 -0
  74. musecbox/res/midi-icon-off.png +0 -0
  75. musecbox/res/midi-icon-on.png +0 -0
  76. musecbox/res/minus.svg +4 -0
  77. musecbox/res/musecbox-icon.png +0 -0
  78. musecbox/res/musescore_score.mscx +1775 -0
  79. musecbox/res/mute.svg +5 -0
  80. musecbox/res/narrow-menu.svg +8 -0
  81. musecbox/res/plus.svg +4 -0
  82. musecbox/res/render.svg +47 -0
  83. musecbox/res/solo.svg +4 -0
  84. musecbox/res/unlock.svg +3 -0
  85. musecbox/score_fixer.py +139 -0
  86. musecbox/scripts/__init__.py +22 -0
  87. musecbox/scripts/mbx_apply.py +66 -0
  88. musecbox/scripts/mbx_project_info.py +71 -0
  89. musecbox/scripts/mbx_track_setup.py +49 -0
  90. musecbox/sfz_previewer.py +118 -0
  91. musecbox/sfzdb.py +400 -0
  92. musecbox/styles/dark.css +404 -0
  93. musecbox/styles/light.css +369 -0
  94. musecbox/styles/system.css +181 -0
  95. musecbox/xdg/install.sh +357 -0
  96. musecbox-0.0.0.dist-info/LICENSE +619 -0
  97. musecbox-0.0.0.dist-info/METADATA +66 -0
  98. musecbox-0.0.0.dist-info/RECORD +100 -0
  99. musecbox-0.0.0.dist-info/WHEEL +5 -0
  100. musecbox-0.0.0.dist-info/entry_points.txt +6 -0
musecbox/__init__.py ADDED
@@ -0,0 +1,383 @@
1
+ # musecbox/__init__.py
2
+ #
3
+ # Copyright 2025 Leon Dionne <ldionne@dridesign.sh.cn>
4
+ #
5
+ # This program is free software; you can redistribute it and/or modify
6
+ # it under the terms of the GNU General Public License as published by
7
+ # the Free Software Foundation; either version 2 of the License, or
8
+ # (at your option) any later version.
9
+ #
10
+ # This program is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with this program; if not, write to the Free Software
17
+ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
18
+ # MA 02110-1301, USA.
19
+ #
20
+ """
21
+ "MusecBox" is a GUI application which hosts .sfz -based synthesizers, designed
22
+ to be tightly integrated with MuseScore.
23
+
24
+ MusecBox utilizes the Carla plugin host application as its back-end. The
25
+ front-end is writted entirely in python, using PyQt.
26
+
27
+ Some features include.:
28
+
29
+ * Multiple MIDI port inputs with up to 16 tracks per port.
30
+ * The ability to create a project by importing a MuseScore3 file.
31
+ * A command line script which allows you to change the MuseScore3 MIDI port
32
+ assignments to match your project.
33
+ * The ability to add any plugin available to Carla to individual tracks.
34
+ * The ability to add shared plugins and route the output of individual tracks
35
+ to the shared plugins.
36
+ * A graphical balance control widget which allows you to visually set the
37
+ location of each instrument within the stereo plane.
38
+ * Super quick project load times.
39
+
40
+ """
41
+ import sys, logging, argparse, glob
42
+ from os.path import join, dirname, basename, realpath, splitext
43
+ from os import linesep
44
+ try:
45
+ from os import startfile
46
+ except ImportError:
47
+ pass
48
+ from platform import system
49
+ from subprocess import Popen
50
+ from tempfile import gettempdir as tempdir
51
+
52
+ # PyQt5 imports
53
+ from PyQt5.QtCore import QSettings
54
+ from PyQt5.QtWidgets import QApplication, QWidget, QSplitter
55
+ from PyQt5.QtGui import QFont
56
+
57
+ from recent_items_list import RecentItemsList
58
+ from simple_carla import (
59
+ PLUGIN_NONE,
60
+ PLUGIN_INTERNAL,
61
+ PLUGIN_LADSPA,
62
+ PLUGIN_DSSI,
63
+ PLUGIN_LV2,
64
+ PLUGIN_VST2,
65
+ PLUGIN_VST3,
66
+ PLUGIN_AU,
67
+ PLUGIN_DLS,
68
+ PLUGIN_GIG,
69
+ PLUGIN_SF2,
70
+ PLUGIN_SFZ,
71
+ PLUGIN_JACK,
72
+ PLUGIN_JSFX,
73
+ PLUGIN_CLAP
74
+ )
75
+ from simple_carla.qt import CarlaQt
76
+ from qt_extras import DevilBox
77
+
78
+ __version__ = "0.0.0"
79
+
80
+ APPLICATION_NAME = 'MusecBox'
81
+ APP_PATH = dirname(realpath(__file__))
82
+ SOCKET_PATH = join(tempdir(), 'musecbox.socket')
83
+ CARRIAGE_RETURN = linesep.encode()
84
+ DEFAULT_STYLE = 'system'
85
+
86
+ # -------------------------------------------------------------------
87
+ # Plugin type lookup dict (see str
88
+
89
+ PLUGIN_TYPE_STRINGS = {
90
+ PLUGIN_INTERNAL: 'Internal',
91
+ PLUGIN_LADSPA: 'LADSPA',
92
+ PLUGIN_DSSI: 'DSSI',
93
+ PLUGIN_LV2: 'LV2',
94
+ PLUGIN_VST2: 'VST2',
95
+ PLUGIN_VST3: 'VST3',
96
+ PLUGIN_AU: 'AU',
97
+ PLUGIN_DLS: 'DLS file',
98
+ PLUGIN_GIG: 'GIG file',
99
+ PLUGIN_SF2: 'SF2 (SoundFont)',
100
+ PLUGIN_SFZ: 'SFZ (Carla)',
101
+ PLUGIN_JACK: 'JACK app',
102
+ PLUGIN_JSFX: 'JSFX',
103
+ PLUGIN_CLAP: 'CLAP'
104
+ }
105
+
106
+ # -----------------------------------------------------------
107
+ # Filetype strings
108
+
109
+ PROJECT_FILE_TYPE = "MusecBox project (*.mbxp)"
110
+ MUSESCORE_FILE_TYPES = "MuseScore score (*.mscz *.mscx)"
111
+ TRACK_DEF_FILE_TYPE = "Tab -separated values (*.tsv)"
112
+ RENDER_FILE_TYPE = "Wav files (*.wav)"
113
+ SFZ_FILE_TYPE = "SFZ instrument definition (*.sfz)"
114
+ SUPPORTED_FILE_TYPES = "All supported files (*.mbxp *.mbxt *.mscz *.mscx *.sfz);;" + \
115
+ "MusecBox project (*.mbxp);;" + \
116
+ "MusecBox track definition (*.mbxt);;" + \
117
+ "MuseScore score (*.mscz *.mscx);;" + \
118
+ "SFZ instruments (*.sfz)"
119
+
120
+ # -----------------------------------------------------------
121
+ # Settings keys
122
+
123
+ KEY_VERTICAL_LAYOUT = 'MainWindow/VerticalLayout'
124
+ KEY_STYLE = 'Style'
125
+ KEY_COPY_SFZS = 'Saving/SFZCopyToProject'
126
+ KEY_SAMPLES_MODE = 'Saving/SFZSamplesMode'
127
+ KEY_CLEAN_SFZS = 'Saving/SFZClean'
128
+ KEY_RECENT_FILES = 'RecentFiles'
129
+ KEY_RECENT_PLUGINS = 'RecentPlugins'
130
+ KEY_RECENT_PROJECT_DIR = 'Dirs/RecentProject'
131
+ KEY_RECENT_SCORE_DIR = 'Dirs/RecentScore'
132
+ KEY_RECENT_EXPORT_DIR = 'Dirs/RecentExport'
133
+ KEY_SFZ_DIR = 'Dirs/SFZFiles'
134
+ KEY_SCORES_DIR = 'Dirs/Scores'
135
+ KEY_SHOW_CHANNELS = 'TrackWidget/ShowChannels'
136
+ KEY_SHOW_INDICATORS = 'TrackWidget/ShowIndicators'
137
+ KEY_SHOW_TOOLBAR = 'MainWindow/ShowToolbar'
138
+ KEY_SHOW_PORT_INPUTS = 'MainWindow/ShowPortInputs'
139
+ KEY_SHOW_BALANCE = 'MainWindow/ShowBalanceControl'
140
+ KEY_SHOW_SHARED_PLUGINS = 'MainWindow/ShowSharedPlugins'
141
+ KEY_SHOW_STATUSBAR = 'MainWindow/ShowStatusbar'
142
+ KEY_SHOW_TRACK_VOLUME = 'MainWindow/ShowTrackVolume'
143
+ KEY_SHOW_PLUGIN_VOLUME = 'MainWindow/ShowPluginVolume'
144
+ KEY_AUTO_CONNECT = 'MainWindow/AutoConnectTracks'
145
+ KEY_AUTO_START = 'MainWindow/AutoStartProject'
146
+ KEY_WATCH_FILES = 'MainWindow/WatchFiles'
147
+ KEY_BCWIDGET_LINES = 'MainWindow/BalanceControlWidgetLines'
148
+ KEY_BCWIDGET_LABELS = 'BalanceControlWidget/ShowLabels'
149
+ KEY_BCWIDGET_TRACKING = 'BalanceControlWidget/HoverTracking'
150
+ KEY_PREVIEW_FILES = 'SFZFileDialog/PreviewFiles'
151
+ KEY_PREVIEWER_MIDI_SRC = 'SFZPreviewer/MIDISource'
152
+ KEY_PREVIEWER_AUDIO_TGT = 'SFZPreviewer/AudioTarget'
153
+
154
+ # -----------------------------------------------------------
155
+ # Keys which are saved on a per-project basis:
156
+
157
+ PROJECT_OPTION_KEYS = [
158
+ KEY_VERTICAL_LAYOUT,
159
+ KEY_STYLE,
160
+ KEY_COPY_SFZS,
161
+ KEY_SAMPLES_MODE,
162
+ KEY_CLEAN_SFZS,
163
+ KEY_RECENT_PLUGINS,
164
+ KEY_RECENT_PROJECT_DIR,
165
+ KEY_RECENT_SCORE_DIR,
166
+ KEY_RECENT_EXPORT_DIR,
167
+ KEY_SFZ_DIR,
168
+ KEY_SCORES_DIR,
169
+ KEY_SHOW_CHANNELS,
170
+ KEY_SHOW_INDICATORS,
171
+ KEY_SHOW_TOOLBAR,
172
+ KEY_SHOW_PORT_INPUTS,
173
+ KEY_SHOW_BALANCE,
174
+ KEY_SHOW_SHARED_PLUGINS,
175
+ KEY_SHOW_STATUSBAR,
176
+ KEY_SHOW_TRACK_VOLUME,
177
+ KEY_SHOW_PLUGIN_VOLUME,
178
+ KEY_BCWIDGET_LINES,
179
+ KEY_WATCH_FILES,
180
+ KEY_PREVIEW_FILES,
181
+ KEY_PREVIEWER_MIDI_SRC,
182
+ KEY_PREVIEWER_AUDIO_TGT
183
+ ]
184
+
185
+ # -----------------------------------------------------------
186
+ # Global texts
187
+
188
+ TEXT_NO_CONN = '- none -'
189
+ TEXT_MULTI_CONN = '* %d *'
190
+ TEXT_CONNECTED_TO = 'Connected to "%s"'
191
+ TEXT_NO_GROUP = '(All sfzs)'
192
+ TEXT_NEW_GROUP = 'New group ...'
193
+
194
+ T_SAMPLEMODE_ABSPATH = 'Point to the original samples - absolute path'
195
+ T_SAMPLEMODE_RELPATH = 'Point to the original samples - relative path'
196
+ T_SAMPLEMODE_COPY = 'Copy samples to a "<project name>-samples" folder'
197
+ T_SAMPLEMODE_SYMLINK = 'Create symlinks in a "<project name>-samples" folder'
198
+ T_SAMPLEMODE_HARDLINK = 'Hardlink the originals in a "<project name>-samples" folder'
199
+
200
+ T_COPY_TO_LOCAL = 'Copy SFZs to a local project folder'
201
+ T_CLEAN_SFZ = 'Remove opcodes not recognized by LiquidSFZ'
202
+
203
+ # -------------------------------------------------------------------
204
+ # Global objects
205
+
206
+ __CARLA = None
207
+ __MAIN_WINDOW = None
208
+ __SFZ_PREVIEWER = None
209
+ __RECENT_PLUGINS = None
210
+ __RECENT_FILES = None
211
+ __SETTINGS = None
212
+ __STYLES = None
213
+
214
+ def carla():
215
+ global __CARLA
216
+ if __CARLA is None:
217
+ __CARLA = CarlaQt(APPLICATION_NAME)
218
+ return __CARLA
219
+
220
+ def set_main_window(window):
221
+ global __MAIN_WINDOW
222
+ __MAIN_WINDOW = window
223
+
224
+ def main_window():
225
+ return __MAIN_WINDOW
226
+
227
+ def previewer():
228
+ from musecbox.sfz_previewer import SFZPreviewer
229
+ global __SFZ_PREVIEWER
230
+ if __SFZ_PREVIEWER is None:
231
+ __SFZ_PREVIEWER = SFZPreviewer()
232
+ __SFZ_PREVIEWER.add_to_carla()
233
+ return __SFZ_PREVIEWER
234
+
235
+ def recent_plugins():
236
+ global __RECENT_PLUGINS
237
+ def sync(items):
238
+ set_setting(KEY_RECENT_PLUGINS, items)
239
+ if __RECENT_PLUGINS is None:
240
+ __RECENT_PLUGINS = RecentItemsList(setting(KEY_RECENT_PLUGINS, list, []))
241
+ __RECENT_PLUGINS.on_change(sync)
242
+ return __RECENT_PLUGINS
243
+
244
+ def recent_files():
245
+ global __RECENT_FILES
246
+ def sync(items):
247
+ set_setting(KEY_RECENT_FILES, items)
248
+ if __RECENT_FILES is None:
249
+ __RECENT_FILES = RecentItemsList(setting(KEY_RECENT_FILES, list, []))
250
+ __RECENT_FILES.on_change(sync)
251
+ return __RECENT_FILES
252
+
253
+ def __settings():
254
+ global __SETTINGS
255
+ if __SETTINGS is None:
256
+ __SETTINGS = QSettings('ZenSoSo', 'musecbox')
257
+ return __SETTINGS
258
+
259
+ def sync_settings():
260
+ __settings().sync()
261
+
262
+ def setting(key, type_ = None, default = None):
263
+ mw = main_window()
264
+ value = mw.option(key) if mw else None
265
+ if value is None:
266
+ value = __settings().value(key, default)
267
+ if value is None and not type_ is None:
268
+ return type_() # Defaults to "False" for bool, "0" for int, etc.
269
+ return type_(value) if type_ else value
270
+
271
+ def set_setting(key, value):
272
+ # TODO(?): Save per-project window geometry
273
+ # MainWindow project_definition takes the setting if possible:
274
+ mw = main_window()
275
+ if mw and main_window().set_option(key, value):
276
+ return
277
+ __settings().setValue(key, value)
278
+
279
+ def styles():
280
+ global __STYLES
281
+ if __STYLES is None:
282
+ __STYLES = {
283
+ splitext(basename(path))[0] : path \
284
+ for path in glob.glob(join(APP_PATH, 'styles', '*.css'))
285
+ }
286
+ return __STYLES
287
+
288
+ def set_application_style():
289
+ style = setting(KEY_STYLE, str, DEFAULT_STYLE)
290
+ with open(styles()[style], 'r', encoding = 'utf-8') as cssfile:
291
+ QApplication.instance().setStyleSheet(cssfile.read())
292
+
293
+ def plugin_display_name(plugin_def):
294
+ """
295
+ Returns a string consisting of the plugin name and type (LADSPA, VST, etc.)
296
+ """
297
+ str_type = PLUGIN_TYPE_STRINGS[plugin_def["type"]] \
298
+ if plugin_def["type"] in PLUGIN_TYPE_STRINGS \
299
+ else 'None'
300
+ return f'"{plugin_def["name"]}" ({str_type})'
301
+
302
+ # -------------------------------------------------------------------
303
+ # Cross-platform open any file / folder with system associated tool
304
+
305
+ def xdg_open(filename):
306
+ if system() == "Windows":
307
+ startfile(filename)
308
+ elif system() == "Darwin":
309
+ Popen(["open", filename])
310
+ else:
311
+ Popen(["xdg-open", filename])
312
+
313
+ # -------------------------------------------------------------------
314
+ # Add extra methods to the QWidget class:
315
+
316
+ def _restore_geometry(widget):
317
+ """
318
+ Restores geometry from musecbox settings using automatically generated key.
319
+ """
320
+ if not hasattr(widget, 'restoreGeometry'):
321
+ return
322
+ geometry = setting(_geometry_key(widget))
323
+ if not geometry is None:
324
+ widget.restoreGeometry(geometry)
325
+ for splitter in widget.findChildren(QSplitter):
326
+ geometry = setting(_splitter_geometry_key(widget, splitter))
327
+ if not geometry is None:
328
+ splitter.restoreState(geometry)
329
+
330
+ def _save_geometry(widget):
331
+ """
332
+ Saves geometry to musecbox settings using automatically generated key.
333
+ """
334
+ if not hasattr(widget, 'saveGeometry'):
335
+ return
336
+ set_setting(_geometry_key(widget), widget.saveGeometry())
337
+ for splitter in widget.findChildren(QSplitter):
338
+ set_setting(_splitter_geometry_key(widget, splitter), splitter.saveState())
339
+
340
+ def _geometry_key(widget):
341
+ """
342
+ Automatic QSettings key generated from class name.
343
+ """
344
+ return f'{type(widget).__name__}/geometry'
345
+
346
+ def _splitter_geometry_key(widget, splitter):
347
+ """
348
+ Automatic QSettings key generated from class name.
349
+ """
350
+ return f'{type(widget).__name__}/{splitter.objectName()}/geometry'
351
+
352
+ QWidget.restore_geometry = _restore_geometry
353
+ QWidget.save_geometry = _save_geometry
354
+
355
+ # -------------------------------------------------------------------
356
+ # Extra GUI funcs:
357
+
358
+ def bold(widget):
359
+ """
360
+ Set the "bold" attribute of the widget font.
361
+ """
362
+ font = widget.font()
363
+ font.setWeight(QFont.Bold)
364
+ widget.setFont(font)
365
+
366
+ def unbold(widget):
367
+ """
368
+ Clear the "bold" attribute of the widget font.
369
+ """
370
+ font = widget.font()
371
+ font.setWeight(QFont.Normal)
372
+ widget.setFont(font)
373
+
374
+ # -------------------------------------------------------------------
375
+ # Custom exceptions:
376
+
377
+ class EngineInitFailure(Exception):
378
+ """
379
+ Raised if carla.engine_init fails
380
+ """
381
+
382
+
383
+ # end musecbox/__init__.py
musecbox/__main__.py ADDED
@@ -0,0 +1,132 @@
1
+ # musecbox/__main__.py
2
+ #
3
+ # Copyright 2025 Leon Dionne <ldionne@dridesign.sh.cn>
4
+ #
5
+ # This program is free software; you can redistribute it and/or modify
6
+ # it under the terms of the GNU General Public License as published by
7
+ # the Free Software Foundation; either version 2 of the License, or
8
+ # (at your option) any later version.
9
+ #
10
+ # This program is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with this program; if not, write to the Free Software
17
+ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
18
+ # MA 02110-1301, USA.
19
+ #
20
+ """
21
+ Application entry point
22
+ """
23
+ import sys, logging, argparse
24
+ from os import environ, unlink
25
+ from os.path import abspath, expanduser
26
+ from socket import socket, AF_UNIX, SOCK_DGRAM, error as sock_error
27
+ from PyQt5.QtWidgets import QApplication
28
+ from qt_extras import DevilBox
29
+ from musecbox import (
30
+ SOCKET_PATH,
31
+ CARRIAGE_RETURN,
32
+ carla,
33
+ EngineInitFailure
34
+ )
35
+ from musecbox.gui.main_window import MainWindow
36
+
37
+
38
+ def main():
39
+ p = argparse.ArgumentParser()
40
+ p.epilog = """
41
+ Hosts multiple LiquidSFZ instances for real-time music generation.
42
+ """
43
+ p.add_argument('Filename', type=str, nargs='?',
44
+ help='MuseScore score to use for port setup, or saved port setup')
45
+ p.add_argument("--horizontal-layout", "-H", action="store_true",
46
+ help="Use standard (horizontal) layout")
47
+ p.add_argument("--vertical-layout", "-V", action="store_true",
48
+ help="Use compact (vertical) layout")
49
+ p.add_argument("--log-file", "-l", type=str,
50
+ help="Log to this file")
51
+ p.add_argument("--verbose", "-v", action="store_true",
52
+ help="Show more detailed debug information")
53
+ options = p.parse_args()
54
+
55
+ # Setup logging
56
+ if 'TERM' in environ:
57
+ log_level = logging.DEBUG if options.verbose else logging.ERROR
58
+ log_file = options.log_file
59
+ else:
60
+ log_level = logging.DEBUG
61
+ log_file = expanduser('~/musecbox.log')
62
+ log_format = "[%(filename)24s:%(lineno)4d] %(levelname)-8s %(message)s"
63
+ if log_file:
64
+ logging.basicConfig(filename = log_file, filemode = 'w',
65
+ level = log_level, format = log_format)
66
+ else:
67
+ logging.basicConfig(level = log_level, format = log_format)
68
+
69
+ #-----------------------------------------------------------------------
70
+ # Annoyance fix per:
71
+ # https://stackoverflow.com/questions/986964/qt-session-management-error
72
+ try:
73
+ del environ['SESSION_MANAGER']
74
+ except KeyError:
75
+ pass
76
+ #-----------------------------------------------------------------------
77
+
78
+ # Connect to running instance:
79
+ sock = socket(AF_UNIX, SOCK_DGRAM)
80
+ try:
81
+ sock.connect(SOCKET_PATH)
82
+ except ConnectionRefusedError:
83
+ unlink(SOCKET_PATH)
84
+ except FileNotFoundError:
85
+ pass
86
+ except sock_error as e:
87
+ logging.error('%s: %s', type(e).__name__, str(e))
88
+ return 1
89
+ else:
90
+ sock.sendall(bytes(abspath(options.Filename) \
91
+ if options.Filename else '???', 'utf-8') + CARRIAGE_RETURN)
92
+ sock.close()
93
+ return 4
94
+ # Delete previous SOCKET_PATH hanging around
95
+ try:
96
+ unlink(SOCKET_PATH)
97
+ except FileNotFoundError:
98
+ pass
99
+
100
+ try:
101
+ sys.getwindowsversion()
102
+ except AttributeError:
103
+ isWindows = False
104
+ else:
105
+ isWindows = True
106
+ if isWindows:
107
+ import win32api, win32process, win32con
108
+ pid = win32api.GetCurrentProcessId()
109
+ handle = win32api.OpenProcess(win32con.PROCESS_ALL_ACCESS, True, pid)
110
+ win32process.SetPriorityClass(handle, win32process.ABOVE_NORMAL_PRIORITY_CLASS)
111
+ else:
112
+ from os import nice
113
+ nice(-10)
114
+
115
+ application = QApplication([])
116
+ try:
117
+ main_window = MainWindow(options)
118
+ except EngineInitFailure as e:
119
+ DevilBox(e)
120
+ return 1
121
+ main_window.show()
122
+ return_value = application.exec()
123
+ unlink(SOCKET_PATH)
124
+ carla().delete()
125
+ return return_value
126
+
127
+
128
+ if __name__ == "__main__":
129
+ sys.exit(main())
130
+
131
+
132
+ # end musecbox/__main__.py
@@ -0,0 +1,63 @@
1
+ # musecbox/audio_recorder.py
2
+ #
3
+ # Copyright 2025 Leon Dionne <ldionne@dridesign.sh.cn>
4
+ #
5
+ # This program is free software; you can redistribute it and/or modify
6
+ # it under the terms of the GNU General Public License as published by
7
+ # the Free Software Foundation; either version 2 of the License, or
8
+ # (at your option) any later version.
9
+ #
10
+ # This program is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with this program; if not, write to the Free Software
17
+ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
18
+ # MA 02110-1301, USA.
19
+ #
20
+ """
21
+ Provides AudioRecorder class
22
+ """
23
+ import logging
24
+ from os.path import join
25
+ from glob import glob
26
+ from shutil import move
27
+ from time import sleep
28
+ from simple_carla.qt import QtPlugin
29
+ from PyQt5.QtCore import QDir
30
+ from musecbox import carla, main_window
31
+
32
+
33
+ class AudioRecorder(QtPlugin):
34
+
35
+ plugin_def = {
36
+ "name" : 'StereoRecord',
37
+ "build" : 2,
38
+ "type" : 4,
39
+ "filename" : 'sc_record.lv2',
40
+ "label" : 'https://github.com/brummer10/screcord#stereo_record',
41
+ "uniqueId" : 0
42
+ }
43
+
44
+ def __init__(self):
45
+ super().__init__()
46
+ self.directory = join(QDir.homePath(), 'lv2record')
47
+ self.startup_files = None
48
+
49
+ def record(self):
50
+ self.parameter('FORM').value = 0.0
51
+ self.parameter('REC').value = 1.0
52
+ self.startup_files = set(glob(f'{self.directory}/*'))
53
+
54
+ def save_as(self, filename):
55
+ self.parameter('REC').value = 0.0
56
+ new_files = set(glob(f'{self.directory}/*')) - self.startup_files
57
+ if len(new_files) == 0:
58
+ raise RuntimeError('Nothing saved by lv2record')
59
+ if len(new_files) > 1:
60
+ raise RuntimeError('Multiple files saved by lv2record')
61
+ move(new_files.pop(), filename)
62
+
63
+ # end musecbox/audio_recorder.py
File without changes