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.
- musecbox/__init__.py +383 -0
- musecbox/__main__.py +132 -0
- musecbox/audio_recorder.py +63 -0
- musecbox/dialogs/__init__ +0 -0
- musecbox/dialogs/add_group_dialog.py +264 -0
- musecbox/dialogs/add_group_dialog.ui +171 -0
- musecbox/dialogs/connection_dialog.py +459 -0
- musecbox/dialogs/connection_dialog.ui +235 -0
- musecbox/dialogs/copy_sfz_dialog.py +152 -0
- musecbox/dialogs/copy_sfz_paths_dialog.py +73 -0
- musecbox/dialogs/copy_sfzs_dialog.py +119 -0
- musecbox/dialogs/copy_sfzs_dialog.ui +146 -0
- musecbox/dialogs/generic_plugin_dialog.py +266 -0
- musecbox/dialogs/instrument_selection_dialog.py +288 -0
- musecbox/dialogs/instrument_selection_dialog.ui +486 -0
- musecbox/dialogs/missing_sfzs_dialog.py +161 -0
- musecbox/dialogs/project_info_dialog.py +103 -0
- musecbox/dialogs/project_info_dialog.ui +107 -0
- musecbox/dialogs/project_load_dialog.py +161 -0
- musecbox/dialogs/project_load_dialog.ui +99 -0
- musecbox/dialogs/project_save_dialog.py +164 -0
- musecbox/dialogs/record_dialog.py +120 -0
- musecbox/dialogs/record_dialog.ui +141 -0
- musecbox/dialogs/score_apply_dialog.py +162 -0
- musecbox/dialogs/score_apply_dialog.ui +171 -0
- musecbox/dialogs/score_import_channel_widget.ui +196 -0
- musecbox/dialogs/score_import_dialog.py +759 -0
- musecbox/dialogs/score_import_dialog.ui +236 -0
- musecbox/dialogs/score_import_part_widget.ui +167 -0
- musecbox/dialogs/score_info_dialog.py +97 -0
- musecbox/dialogs/score_info_dialog.ui +117 -0
- musecbox/dialogs/score_load_dialog.py +114 -0
- musecbox/dialogs/sfz_file_dialog.py +411 -0
- musecbox/dialogs/sfz_file_dialog.ui +284 -0
- musecbox/dialogs/sfzdb_dialog.py +289 -0
- musecbox/dialogs/sfzdb_dialog.ui +250 -0
- musecbox/dialogs/track_creation_dialog.py +87 -0
- musecbox/dialogs/track_creation_dialog.ui +190 -0
- musecbox/gui/__init__.py +37 -0
- musecbox/gui/balance_control_widget.py +627 -0
- musecbox/gui/horizontal_port_widget.ui +130 -0
- musecbox/gui/horizontal_track_plugin_widget.ui +273 -0
- musecbox/gui/horizontal_track_widget.ui +207 -0
- musecbox/gui/main_window.py +1508 -0
- musecbox/gui/main_window.ui +876 -0
- musecbox/gui/plugin_widgets.py +861 -0
- musecbox/gui/port_widget.py +528 -0
- musecbox/gui/shared_plugin_widget.ui +290 -0
- musecbox/gui/track_widget.py +783 -0
- musecbox/gui/vertical_port_widget.ui +138 -0
- musecbox/gui/vertical_track_plugin_widget.ui +273 -0
- musecbox/gui/vertical_track_widget.ui +240 -0
- musecbox/liquidsfz.py +68 -0
- musecbox/res/Theremin/Stereo-Theremin.sfz +77 -0
- musecbox/res/Theremin/Thick-Theremin.sfz +40 -0
- musecbox/res/Theremin/Thin-Theremin.sfz +43 -0
- musecbox/res/Theremin/samples/SINUS.wav +0 -0
- musecbox/res/Theremin/samples/U-sinewave.wav +0 -0
- musecbox/res/audio-icon-off.png +0 -0
- musecbox/res/audio-icon-on.png +0 -0
- musecbox/res/balance_control_widget.h +13 -0
- musecbox/res/collapse-vertical.svg +34 -0
- musecbox/res/collapse.svg +35 -0
- musecbox/res/control-icon-off.png +0 -0
- musecbox/res/control-icon-on.png +0 -0
- musecbox/res/disable-off.png +0 -0
- musecbox/res/disable-on.png +0 -0
- musecbox/res/empty.sfz +4 -0
- musecbox/res/expand-vertical.svg +34 -0
- musecbox/res/expand.svg +38 -0
- musecbox/res/lock.svg +3 -0
- musecbox/res/menu.svg +8 -0
- musecbox/res/meter.png +0 -0
- musecbox/res/midi-icon-off.png +0 -0
- musecbox/res/midi-icon-on.png +0 -0
- musecbox/res/minus.svg +4 -0
- musecbox/res/musecbox-icon.png +0 -0
- musecbox/res/musescore_score.mscx +1775 -0
- musecbox/res/mute.svg +5 -0
- musecbox/res/narrow-menu.svg +8 -0
- musecbox/res/plus.svg +4 -0
- musecbox/res/render.svg +47 -0
- musecbox/res/solo.svg +4 -0
- musecbox/res/unlock.svg +3 -0
- musecbox/score_fixer.py +139 -0
- musecbox/scripts/__init__.py +22 -0
- musecbox/scripts/mbx_apply.py +66 -0
- musecbox/scripts/mbx_project_info.py +71 -0
- musecbox/scripts/mbx_track_setup.py +49 -0
- musecbox/sfz_previewer.py +118 -0
- musecbox/sfzdb.py +400 -0
- musecbox/styles/dark.css +404 -0
- musecbox/styles/light.css +369 -0
- musecbox/styles/system.css +181 -0
- musecbox/xdg/install.sh +357 -0
- musecbox-0.0.0.dist-info/LICENSE +619 -0
- musecbox-0.0.0.dist-info/METADATA +66 -0
- musecbox-0.0.0.dist-info/RECORD +100 -0
- musecbox-0.0.0.dist-info/WHEEL +5 -0
- 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
|