kitbash 1.5.1__tar.gz → 1.7.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.
@@ -1,18 +1,16 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: kitbash
3
- Version: 1.5.1
3
+ Version: 1.7.0
4
4
  Summary: Provides universal settings, styles, cached icons, and pixmaps, and the base
5
5
  Author-email: Leon Dionne <ldionne@dridesign.sh.cn>
6
6
  Description-Content-Type: text/markdown
7
7
  Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
8
+ Requires-Dist: log_soso >= 1.0.3
8
9
  Requires-Dist: PyQt5
9
- Requires-Dist: conn_jack >= 1.2.0, <= 1.3.0
10
- Requires-Dist: liquiphy
11
- Requires-Dist: soundfile
12
- Requires-Dist: sfzen >= 2.2.0
13
- Requires-Dist: midi_notes >= 1.1.0
14
- Requires-Dist: recent_items_list >= 1.1.1
15
- Requires-Dist: soso_qt_extras >= 1.2.0
10
+ Requires-Dist: qt_liquid_pool >= 1.2.0
11
+ Requires-Dist: recent_items_list >= 1.1.2
12
+ Requires-Dist: sfzen >= 2.3.1
13
+ Requires-Dist: soso_qt_extras >= 1.7.0
16
14
  Project-URL: Home, https://github.com/Zen-Master-SoSo/kitbash
17
15
 
18
16
  # KitBash
@@ -28,14 +28,14 @@ from functools import lru_cache
28
28
  from PyQt5.QtCore import QSettings
29
29
  from PyQt5.QtWidgets import QApplication, QSplitter
30
30
  from qt_extras import DevilBox
31
- from conn_jack import JackConnectError
32
31
 
33
- __version__ = "1.5.1"
32
+ __version__ = "1.7.0"
34
33
 
35
34
  APPLICATION_NAME = "kitbash"
36
35
  PACKAGE_DIR = dirname(__file__)
37
36
  DEFAULT_STYLE = 'system'
38
37
  AUDIO_ICON_SIZE = 16
38
+ KEY_PREVIEW_VELOCITY = 'PreviewVelocity'
39
39
  KEY_STYLE = 'Style'
40
40
  KEY_SAMPLES_MODE = 'KitSaveDialog/SamplesMode'
41
41
  KEY_RECENT_DRUMKIT_FOLDER = 'RecentDrumkitFolder'
@@ -44,8 +44,16 @@ KEY_RECENT_PROJECT_FOLDER = 'RecentProjectFolder'
44
44
  KEY_RECENT_PROJECTS = 'RecentProjects'
45
45
  KEY_SAMPLE_XPLORE_ROOT = 'SampleExplorer/Root'
46
46
  KEY_SAMPLE_XPLORE_CURR = 'SampleExplorer/Current'
47
+ KEY_MIDI_SOURCE = 'MIDISource'
48
+ KEY_AUDIO_SINK = 'AudioSink'
47
49
 
48
50
 
51
+ def audio():
52
+ if not hasattr(audio, 'instance'):
53
+ from kitbash.jack_audio import Audio
54
+ audio.instance = Audio()
55
+ return audio.instance
56
+
49
57
  @lru_cache
50
58
  def __settings():
51
59
  return QSettings('ZenSoSo', APPLICATION_NAME)
@@ -22,9 +22,8 @@ kitbash is a program you can use to combine parts of various SFZ files into a
22
22
  new SFZ with instruments "borrowed" from the originals.
23
23
  """
24
24
  import sys, os, argparse, logging
25
- from PyQt5.QtWidgets import QApplication
26
- from qt_extras import DevilBox
27
- from conn_jack import JackConnectError
25
+ from PyQt5.QtWidgets import QApplication, QErrorMessage
26
+ from qt_extras import exceptions_hook
28
27
  from kitbash.gui.main_window import MainWindow
29
28
 
30
29
 
@@ -61,11 +60,8 @@ def main():
61
60
  #-----------------------------------------------------------------------
62
61
 
63
62
  app = QApplication([])
64
- try:
65
- main_window = MainWindow(options)
66
- except JackConnectError:
67
- DevilBox('Could not connect to JACK server. Is it running?')
68
- sys.exit(1)
63
+ sys.excepthook = exceptions_hook
64
+ main_window = MainWindow(options)
69
65
  main_window.show()
70
66
  sys.exit(app.exec())
71
67
 
@@ -29,7 +29,7 @@ from PyQt5.QtWidgets import (QApplication, QVBoxLayout, QHBoxLayout, QLabel,
29
29
  QFrame, QSizePolicy, QPushButton, QCheckBox)
30
30
  from qt_extras import SigBlock
31
31
  from sfzen.drumkits import PercussionInstrument
32
- from kitbash import PACKAGE_DIR
32
+ from kitbash import PACKAGE_DIR, KEY_PREVIEW_VELOCITY, get_setting, audio
33
33
 
34
34
 
35
35
  @lru_cache
@@ -64,9 +64,9 @@ class DrumkitWidget(QFrame):
64
64
  self.sfz_filename = filename
65
65
  self.moniker = basename(self.sfz_filename)
66
66
  self.drumkit = None
67
- self.port_number = None
68
- self.initial_height = None
67
+ self.synth = None
69
68
 
69
+ self.initial_height = None
70
70
  self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Preferred)
71
71
 
72
72
  main_layout = QVBoxLayout()
@@ -130,6 +130,7 @@ class DrumkitWidget(QFrame):
130
130
  Fills the groups and instruments of this the DrumkitWidget.
131
131
  """
132
132
  self.drumkit = drumkit
133
+ self.synth = audio().create_synth(self.sfz_filename)
133
134
  for group in self.drumkit.percussion_groups.values():
134
135
  if group.is_empty():
135
136
  continue
@@ -162,14 +163,21 @@ class DrumkitWidget(QFrame):
162
163
  """
163
164
  Triggered by InstrumentButton mouse press.
164
165
  """
165
- self.sig_note_on.emit(inst.pitch)
166
+ self.synth.noteon(
167
+ self.drumkit.midi_channel,
168
+ inst.pitch,
169
+ get_setting(KEY_PREVIEW_VELOCITY, 80, int)
170
+ )
166
171
 
167
172
  @pyqtSlot(PercussionInstrument)
168
173
  def slot_instrument_released(self, inst):
169
174
  """
170
175
  Triggered by InstrumentButton mouse relase.
171
176
  """
172
- self.sig_note_off.emit(inst.pitch)
177
+ self.synth.noteoff(
178
+ self.drumkit.midi_channel,
179
+ inst.pitch
180
+ )
173
181
 
174
182
  @pyqtSlot(QPushButton)
175
183
  def slot_instrument_toggled(self, button):
@@ -187,6 +195,7 @@ class DrumkitWidget(QFrame):
187
195
  """
188
196
  Triggered by the "remove" button click event.
189
197
  """
198
+ audio().delete_synth(self.synth)
190
199
  self.sig_remove_drumkit.emit(self)
191
200
 
192
201
  @pyqtSlot(bool)
@@ -20,7 +20,6 @@
20
20
  """
21
21
  Provides KitSaveDialog.
22
22
  """
23
- import logging
24
23
  from os.path import realpath, splitext
25
24
  from functools import partial
26
25
  from PyQt5.QtCore import Qt, pyqtSlot, QCoreApplication
@@ -31,18 +31,18 @@ from PyQt5.QtCore import Qt, QObject, pyqtSlot, QTimer, QThreadPool, QPoint, QCo
31
31
  from PyQt5.QtWidgets import (
32
32
  QApplication, QMainWindow, QMessageBox, QFileDialog, QAction, QActionGroup, QMenu)
33
33
  from PyQt5.QtGui import QIcon
34
- from qt_extras import ShutUpQT, SigBlock, DevilBox
34
+ from qt_extras import ShutUpQT, DevilBox
35
35
  from qt_extras.list_layout import VListLayout
36
36
  from recent_items_list import RecentItemsList
37
37
  from sfzen import SAMPLES_ABSPATH
38
38
  from sfzen.drumkits import Drumkit
39
39
  from kitbash import (
40
40
  APPLICATION_NAME, PACKAGE_DIR, DEFAULT_STYLE,
41
- styles, set_application_style, get_setting, set_setting, GeometrySaver,
42
- KEY_STYLE, KEY_SAMPLES_MODE, KEY_RECENT_DRUMKIT_FOLDER, KEY_RECENT_DRUMKITS,
41
+ styles, set_application_style, get_setting, set_setting, GeometrySaver, audio,
42
+ KEY_PREVIEW_VELOCITY, KEY_STYLE, KEY_SAMPLES_MODE,
43
+ KEY_RECENT_DRUMKIT_FOLDER, KEY_RECENT_DRUMKITS,
43
44
  KEY_RECENT_PROJECT_FOLDER, KEY_RECENT_PROJECTS)
44
45
  from kitbash.worker_threads import KitLoader, KitBasher
45
- from kitbash.jack_audio import Audio
46
46
  from kitbash.gui.drumkit_widget import DrumkitWidget
47
47
  from kitbash.gui.kit_save_dialog import KitSaveDialog
48
48
 
@@ -94,7 +94,18 @@ class MainWindow(QMainWindow, GeometrySaver):
94
94
  # Setup background threadpool for KitLoader and KitBasher workers
95
95
  self.background_threadpool = QThreadPool()
96
96
  # Setup jack audio
97
- self.audio = Audio()
97
+ self.audio = audio()
98
+ # Setup source/sink combo boxes:
99
+ self.cmb_midi_sources = self.audio.midi_in_combo_box(self)
100
+ self.lo_source_combo.replaceWidget(self.cmb_midi_src_placeholder, self.cmb_midi_sources)
101
+ self.cmb_midi_src_placeholder.setVisible(False)
102
+ self.cmb_midi_src_placeholder.deleteLater()
103
+ del self.cmb_midi_src_placeholder
104
+ self.cmb_audio_sinks = self.audio.audio_out_combo_box(self)
105
+ self.lo_sink_combo.replaceWidget(self.cmb_audio_sink_placeholder, self.cmb_audio_sinks)
106
+ self.cmb_audio_sink_placeholder.setVisible(False)
107
+ self.cmb_audio_sink_placeholder.deleteLater()
108
+ del self.cmb_audio_sink_placeholder
98
109
  # Setup GUI elements
99
110
  self.fill_style_menu()
100
111
  self.setup_kits_area()
@@ -122,7 +133,7 @@ class MainWindow(QMainWindow, GeometrySaver):
122
133
  self.action_save_project.triggered.connect(self.slot_save_project)
123
134
  self.action_save_project_as.triggered.connect(self.slot_save_project_as)
124
135
  self.action_save_bashed_kit.triggered.connect(self.slot_save_kit)
125
- self.action_save_kit_as.triggered.connect(self.slot_save_kit_as)
136
+ self.action_save_bashed_kit_as.triggered.connect(self.slot_save_kit_as)
126
137
  self.action_add_drumkit.triggered.connect(self.slot_add_drumkit)
127
138
  self.action_remove_all_kits.triggered.connect(self.slot_remove_all_kits)
128
139
  self.action_reload_style.triggered.connect(self.slot_reload_style)
@@ -137,14 +148,8 @@ class MainWindow(QMainWindow, GeometrySaver):
137
148
  self.b_copy_path.clicked.connect(self.slot_copy_kit_path)
138
149
  self.b_add_drumkit.clicked.connect(self.slot_add_drumkit)
139
150
  self.b_xruns.clicked.connect(self.slot_xruns_clicked)
140
- self.cmb_midi_srcs.currentTextChanged.connect(
141
- self.audio.slot_midi_src_selected)
142
- self.cmb_audio_sinks.currentTextChanged.connect(
143
- self.audio.slot_audio_sink_selected)
144
- self.audio.sig_sources_changed.connect(
145
- self.slot_sources_changed)
146
- self.audio.sig_sinks_changed.connect(
147
- self.slot_sinks_changed)
151
+ self.spn_velocity.valueChanged.connect(self.slot_velocity_value_changed)
152
+ self.audio.sig_jack_ready.connect(self.slot_jack_ready)
148
153
 
149
154
  def update_ui(self):
150
155
  title = APPLICATION_NAME \
@@ -158,7 +163,11 @@ class MainWindow(QMainWindow, GeometrySaver):
158
163
  self.action_new_project.setEnabled(has_kits)
159
164
  self.action_save_project.setEnabled(has_kits and self.dirty)
160
165
  self.action_save_project_as.setEnabled(has_kits)
161
- self.action_save_bashed_kit.setEnabled(has_kits)
166
+ self.action_save_bashed_kit.setEnabled(has_kits and self.saved_sfz_filename is not None)
167
+ self.action_save_bashed_kit.setText(
168
+ 'Save bashed kit' if self.saved_sfz_filename is None
169
+ else f'Save "{self.saved_sfz_filename}"')
170
+ self.action_save_bashed_kit_as.setEnabled(has_kits)
162
171
  self.b_save_project.setEnabled(has_kits)
163
172
  self.b_save_kit.setEnabled(has_kits)
164
173
  self.b_copy_path.setVisible(bool(self.saved_sfz_filename))
@@ -218,7 +227,6 @@ class MainWindow(QMainWindow, GeometrySaver):
218
227
  Starts project load; saves recent file name.
219
228
  Permission to clear must already have been given.
220
229
  """
221
- logging.debug('load_project %s', filename)
222
230
  if exists(filename):
223
231
  try:
224
232
  with open(filename, 'r', encoding = 'utf-8') as fh:
@@ -251,18 +259,6 @@ class MainWindow(QMainWindow, GeometrySaver):
251
259
  self.set_dirty(False)
252
260
  self.statusbar.showMessage(f'Saved project at {self.project_filename}', MESSAGE_TIMEOUT)
253
261
 
254
- def save_kit(self):
255
- kit = self.bashed_kit.simplified()
256
- kit.default_path = dirname(self.saved_sfz_filename)
257
- try:
258
- kit.save_as(self.saved_sfz_filename, self.saved_sfz_samples_mode)
259
- logging.debug('Saved bashed SFZ at %s', self.saved_sfz_filename)
260
- self.statusbar.showMessage(f'Saved {self.saved_sfz_filename}', MESSAGE_TIMEOUT)
261
- except OSError as e:
262
- DevilBox('Hardlinks between devices are not allowed.\n' +\
263
- 'Choose a different path or sample mode.' if e.errno == 18 \
264
- else str(e))
265
-
266
262
  def register_recent_project(self):
267
263
  self.recent_projects.bump(self.project_filename)
268
264
  set_setting(KEY_RECENT_PROJECT_FOLDER, dirname(self.project_filename))
@@ -332,41 +328,10 @@ class MainWindow(QMainWindow, GeometrySaver):
332
328
  # -----------------------------------------------------------------
333
329
  # JACK audio / source / sink management
334
330
 
335
- @pyqtSlot(int)
336
- def slot_jack_ready(self, samplerate):
337
- self.lbl_jack_state.setText(f'JACK samplerate: {samplerate}')
338
-
339
- @pyqtSlot()
340
- def slot_jack_down(self):
341
- self.lbl_jack_state.setText('JACK is down')
342
-
343
- @pyqtSlot()
344
- def slot_sources_changed(self):
345
- with SigBlock(self.cmb_midi_srcs):
346
- self.cmb_midi_srcs.clear()
347
- self.cmb_midi_srcs.addItem('')
348
- for port in self.audio.conn_man.output_ports():
349
- if port.is_midi:
350
- self.cmb_midi_srcs.addItem(port.name)
351
- if self.audio.synth.connected_midi_src_port:
352
- self.cmb_midi_srcs.setCurrentText(self.audio.midi_src)
353
-
354
- @pyqtSlot()
355
- def slot_sinks_changed(self):
356
- with SigBlock(self.cmb_audio_sinks):
357
- self.cmb_audio_sinks.clear()
358
- self.cmb_audio_sinks.addItem('')
359
- valid_clients = set(
360
- port.client_name for port in self.audio.conn_man.input_ports()
361
- if port.is_audio )
362
- for client in valid_clients:
363
- self.cmb_audio_sinks.addItem(client)
364
- if self.audio.synth.connected_audio_sink_ports:
365
- self.cmb_audio_sinks.setCurrentText(self.audio.audio_sink)
366
-
367
- @pyqtSlot(str)
368
- def slot_midi_connected(self, port_name):
369
- self.statusbar.showMessage(f'Connected to "{port_name}"', MESSAGE_TIMEOUT)
331
+ @pyqtSlot(bool, int)
332
+ def slot_jack_ready(self, state, _):
333
+ if not state:
334
+ DevilBox('JACK is down')
370
335
 
371
336
  # -----------------------------------------------------------------
372
337
  # Drumkit load / delete / instrument selection
@@ -402,8 +367,6 @@ class MainWindow(QMainWindow, GeometrySaver):
402
367
  """
403
368
  drumkit_widget.set_drumkit(drumkit)
404
369
  drumkit_widget.sig_inst_toggle.connect(self.slot_inst_toggle)
405
- drumkit_widget.sig_note_on.connect(self.slot_note_on)
406
- drumkit_widget.sig_note_off.connect(self.slot_note_off)
407
370
  drumkit_widget.sig_remove_drumkit.connect(self.slot_remove_drumkit)
408
371
  if self.project_loading:
409
372
  drumkit_widget.apply_selections(
@@ -468,26 +431,23 @@ class MainWindow(QMainWindow, GeometrySaver):
468
431
  Triggered from KitBasher signal when bashing is finished.
469
432
  """
470
433
  self.bashed_kit = bashed_kit
471
- with open(self.tempfile, 'w') as fob:
472
- self.bashed_kit.write(fob)
473
- self.audio.synth.load(self.tempfile) # pylint: disable = no-member
434
+ self.audio.load_kit(self.bashed_kit)
474
435
  self.statusbar.showMessage('Drumkit updated', MESSAGE_TIMEOUT)
475
436
 
476
- @pyqtSlot(int)
477
- def slot_note_on(self, pitch):
478
- self.audio.synth.noteon(0, pitch, self.spn_velocity.value())
479
-
480
- @pyqtSlot(int)
481
- def slot_note_off(self, pitch):
482
- self.audio.synth.noteoff(0, pitch)
483
-
484
437
  # -----------------------------------------------------------------
485
438
  # UI handling slots:
486
439
 
440
+ @pyqtSlot(int)
441
+ def slot_velocity_value_changed(self, value):
442
+ """
443
+ Triggered by spn_velocity.valueChanged
444
+ """
445
+ set_setting(KEY_PREVIEW_VELOCITY, value)
446
+
487
447
  @pyqtSlot()
488
448
  def slot_xruns_clicked(self):
489
449
  """
490
- Triggered by b_xruns.click()
450
+ Triggered by b_xruns.click
491
451
  """
492
452
  self.base_xruns = self.current_xruns
493
453
  self.b_xruns.setText('0')
@@ -616,15 +576,26 @@ class MainWindow(QMainWindow, GeometrySaver):
616
576
  @pyqtSlot()
617
577
  def slot_save_kit(self):
618
578
  if self.saved_sfz_filename is None:
619
- self.slot_save_kit_as()
579
+ return self.slot_save_kit_as()
580
+ try:
581
+ self.bashed_kit.save_as(
582
+ self.saved_sfz_filename, samples_mode = self.saved_sfz_samples_mode)
583
+ except OSError as e:
584
+ if e.errno == 18:
585
+ DevilBox('Hardlinks between devices are not allowed.\n' +
586
+ 'Choose a different path or sample mode.')
587
+ elif e.errno == 2:
588
+ return self.slot_save_kit_as()
589
+ else:
590
+ DevilBox(str(e))
620
591
  else:
621
- self.save_kit()
592
+ self.statusbar.showMessage(f'Saved {self.saved_sfz_filename}', MESSAGE_TIMEOUT)
593
+ self.update_ui()
622
594
 
623
595
  @pyqtSlot()
624
596
  def slot_save_kit_as(self):
625
597
  """
626
598
  Triggered by "File -> Save bashed kit" menu
627
- See also: slot_drumkit_bashed
628
599
  """
629
600
  dlg = KitSaveDialog(self,
630
601
  int(get_setting(KEY_SAMPLES_MODE, SAMPLES_ABSPATH)) \
@@ -633,7 +604,7 @@ class MainWindow(QMainWindow, GeometrySaver):
633
604
  if dlg.exec_() and dlg.selected_file:
634
605
  self.saved_sfz_filename = dlg.selected_file
635
606
  self.saved_sfz_samples_mode = dlg.samples_mode
636
- self.save_kit()
607
+ self.slot_save_kit()
637
608
 
638
609
  @pyqtSlot()
639
610
  def slot_add_drumkit(self):
@@ -180,7 +180,7 @@
180
180
  </spacer>
181
181
  </item>
182
182
  <item>
183
- <layout class="QHBoxLayout" name="hz1">
183
+ <layout class="QHBoxLayout" name="lo_source_combo">
184
184
  <property name="spacing">
185
185
  <number>3</number>
186
186
  </property>
@@ -192,7 +192,7 @@
192
192
  </widget>
193
193
  </item>
194
194
  <item>
195
- <widget class="QComboBox" name="cmb_midi_srcs">
195
+ <widget class="QComboBox" name="cmb_midi_src_placeholder">
196
196
  <property name="toolTip">
197
197
  <string>Get MIDI input from this JACK client</string>
198
198
  </property>
@@ -204,7 +204,7 @@
204
204
  </layout>
205
205
  </item>
206
206
  <item>
207
- <layout class="QHBoxLayout" name="hz2">
207
+ <layout class="QHBoxLayout" name="lo_sink_combo">
208
208
  <property name="spacing">
209
209
  <number>3</number>
210
210
  </property>
@@ -216,7 +216,7 @@
216
216
  </widget>
217
217
  </item>
218
218
  <item>
219
- <widget class="QComboBox" name="cmb_audio_sinks">
219
+ <widget class="QComboBox" name="cmb_audio_sink_placeholder">
220
220
  <property name="toolTip">
221
221
  <string>Send audio out to this JACK client</string>
222
222
  </property>
@@ -314,7 +314,7 @@
314
314
  <x>0</x>
315
315
  <y>0</y>
316
316
  <width>767</width>
317
- <height>390</height>
317
+ <height>389</height>
318
318
  </rect>
319
319
  </property>
320
320
  </widget>
@@ -331,7 +331,7 @@
331
331
  <height>22</height>
332
332
  </rect>
333
333
  </property>
334
- <widget class="QMenu" name="menu_File">
334
+ <widget class="QMenu" name="menu_file">
335
335
  <property name="title">
336
336
  <string>&amp;File</string>
337
337
  </property>
@@ -351,11 +351,11 @@
351
351
  <addaction name="menu_recent_project"/>
352
352
  <addaction name="separator"/>
353
353
  <addaction name="action_save_bashed_kit"/>
354
- <addaction name="action_save_kit_as"/>
354
+ <addaction name="action_save_bashed_kit_as"/>
355
355
  <addaction name="separator"/>
356
356
  <addaction name="action_quit"/>
357
357
  </widget>
358
- <widget class="QMenu" name="menu_View">
358
+ <widget class="QMenu" name="menu_view">
359
359
  <property name="title">
360
360
  <string>&amp;View</string>
361
361
  </property>
@@ -373,7 +373,7 @@
373
373
  <addaction name="menu_style"/>
374
374
  <addaction name="action_reload_style"/>
375
375
  </widget>
376
- <widget class="QMenu" name="menu_Edit">
376
+ <widget class="QMenu" name="menu_edit">
377
377
  <property name="title">
378
378
  <string>&amp;Edit</string>
379
379
  </property>
@@ -391,9 +391,9 @@
391
391
  <addaction name="separator"/>
392
392
  <addaction name="action_remove_all_kits"/>
393
393
  </widget>
394
- <addaction name="menu_File"/>
395
- <addaction name="menu_Edit"/>
396
- <addaction name="menu_View"/>
394
+ <addaction name="menu_file"/>
395
+ <addaction name="menu_edit"/>
396
+ <addaction name="menu_view"/>
397
397
  </widget>
398
398
  <widget class="QStatusBar" name="statusbar"/>
399
399
  <action name="action_add_drumkit">
@@ -537,7 +537,7 @@
537
537
  <string>Remove All Drumkits</string>
538
538
  </property>
539
539
  </action>
540
- <action name="action_save_kit_as">
540
+ <action name="action_save_bashed_kit_as">
541
541
  <property name="text">
542
542
  <string>Save Bashed Kit &amp;As ...</string>
543
543
  </property>
@@ -0,0 +1,78 @@
1
+ # kitbash/kitbash/jack_audio.py
2
+ #
3
+ # Copyright 2026 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 MainWindow of the kitbash application.
22
+ """
23
+ import logging # pylint: disable = unused-import
24
+ from tempfile import mkstemp
25
+ from os import unlink
26
+ from qt_liquid_pool import LiquidPool
27
+ from kitbash import get_setting, set_setting, KEY_MIDI_SOURCE, KEY_AUDIO_SINK
28
+
29
+
30
+ class Audio(LiquidPool):
31
+ """
32
+ Handles audio, including hosting LiquidSFZ instances.
33
+ """
34
+
35
+ def __init__(self):
36
+ super().__init__()
37
+ _, self.tempfile = mkstemp(suffix='.sfz')
38
+ self.kit_synth = None
39
+ self.sig_jack_ready.connect(self.slot_jack_ready)
40
+
41
+ def quit(self):
42
+ super().quit()
43
+ unlink(self.tempfile)
44
+
45
+ def slot_jack_ready(self, state):
46
+ if state:
47
+ with open(self.tempfile, 'w', encoding = 'utf-8') as fob:
48
+ fob.write('// Empty\n')
49
+ self.kit_synth = self.create_synth(self.tempfile)
50
+
51
+ def get_preferred_midi_source(self):
52
+ return get_setting(KEY_MIDI_SOURCE)
53
+
54
+ def set_preferred_midi_source(self, value):
55
+ set_setting(KEY_MIDI_SOURCE, value)
56
+ super().set_preferred_midi_source(value)
57
+
58
+ def get_preferred_audio_sink(self):
59
+ return get_setting(KEY_AUDIO_SINK)
60
+
61
+ def set_preferred_audio_sink(self, value):
62
+ set_setting(KEY_AUDIO_SINK, value)
63
+ super().set_preferred_audio_sink(value)
64
+
65
+ def connect_midi_source(self, port):
66
+ """
67
+ Override in order to prevent all but the main window bashed drumkit synth from
68
+ connecting to the midi source.
69
+ """
70
+ self.conn_man.connect(port, self.kit_synth.input_port)
71
+
72
+ def load_kit(self, drumkit):
73
+ with open(self.tempfile, 'w', encoding = 'utf-8') as fob:
74
+ drumkit.write(fob)
75
+ self.kit_synth.load(self.tempfile) # pylint: disable = no-member
76
+
77
+
78
+ # end kitbash/kitbash/jack_audio.py
@@ -18,9 +18,8 @@
18
18
  # MA 02110-1301, USA.
19
19
  #
20
20
  """
21
- Provides MainWindow.
21
+ Provides worker threads that load and bash drumkits.
22
22
  """
23
- import logging
24
23
  from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, QRunnable
25
24
  from sfzen.drumkits import Drumkit
26
25
 
@@ -36,7 +35,7 @@ class KitWorkerSignals(QObject):
36
35
 
37
36
  class KitLoader(QRunnable):
38
37
  """
39
- Loads a Drumkit in a background thread and emits "sig_drumkit_loaded" when done.
38
+ Loads a Drumkit in a background thread.
40
39
  """
41
40
 
42
41
  def __init__(self, drumkit_widget):
@@ -47,6 +46,7 @@ class KitLoader(QRunnable):
47
46
  @pyqtSlot()
48
47
  def run(self):
49
48
  drumkit = Drumkit(self.drumkit_widget.sfz_filename)
49
+ drumkit.midi_channel = 10 if 'lochan' in drumkit.opcodes_used() else 0
50
50
  self.signals.sig_loaded.emit(self.drumkit_widget, drumkit)
51
51
 
52
52
 
@@ -6,14 +6,12 @@ license = {file = "LICENSE"}
6
6
  classifiers = ["License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)"]
7
7
  dynamic = ["version", "description"]
8
8
  dependencies = [
9
+ "log_soso >= 1.0.3",
9
10
  "PyQt5",
10
- "conn_jack >= 1.2.0, <= 1.3.0",
11
- "liquiphy",
12
- "soundfile",
13
- "sfzen >= 2.2.0",
14
- "midi_notes >= 1.1.0",
15
- "recent_items_list >= 1.1.1",
16
- "soso_qt_extras >= 1.2.0"
11
+ "qt_liquid_pool >= 1.2.0",
12
+ "recent_items_list >= 1.1.2",
13
+ "sfzen >= 2.3.1",
14
+ "soso_qt_extras >= 1.7.0"
17
15
  ]
18
16
 
19
17
  [project.urls]
@@ -28,7 +26,7 @@ requires = ["flit_core >=3.2,<4"]
28
26
  build-backend = "flit_core.buildapi"
29
27
 
30
28
  [bumpver]
31
- current_version = "1.5.1"
29
+ current_version = "1.7.0"
32
30
  version_pattern = "MAJOR.MINOR.PATCH"
33
31
  commit_message = "Bump version {old_version} -> {new_version}"
34
32
  commit = true
@@ -1,243 +0,0 @@
1
- # kitbash/kitbash/jack_audio.py
2
- #
3
- # Copyright 2026 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 MainWindow of the kitstarter application.
22
- """
23
- import logging, tempfile
24
- from os import unlink
25
- from PyQt5.QtCore import Qt, QObject, pyqtSignal, pyqtSlot, QTimer
26
- from liquiphy import LiquidSFZ
27
- from conn_jack import JackConnectionManager, JackConnectError
28
- from kitstarter import get_setting, set_setting, KEY_MIDI_SOURCE, KEY_AUDIO_SINK
29
-
30
-
31
- SYNTH_NAME = 'liquidsfz'
32
- AUDIO_PLAYER_CLIENT = 'kitstarter_jack_player'
33
- CONNECT_RETRY_INTERVAL = 1776
34
-
35
-
36
- class Audio(QObject):
37
- """
38
- Handles audio, including hosting an instance of liquidsfz, and playing samples.
39
- """
40
-
41
- sig_ports_complete = pyqtSignal() # \
42
- sig_sources_changed = pyqtSignal() # Used to decouple JackConnectionManager callbacks
43
- sig_sinks_changed = pyqtSignal() # /
44
- sig_jack_down = pyqtSignal() #
45
- sig_jack_ready = pyqtSignal(int)
46
- sig_midi_connected = pyqtSignal(str)
47
-
48
- def __init__(self):
49
- super().__init__()
50
- self.conn_man = None
51
- self.synth = None
52
- _, self.tempfile = tempfile.mkstemp(suffix='.sfz')
53
- self.sig_ports_complete.connect(self.slot_ports_complete, type = Qt.QueuedConnection)
54
- self.sig_sources_changed.connect(self.slot_sources_changed, type = Qt.QueuedConnection)
55
- self.sig_sinks_changed.connect(self.slot_sinks_changed, type = Qt.QueuedConnection)
56
- self.sig_jack_down.connect(self.slot_jack_down, type = Qt.QueuedConnection)
57
- self.connect_retry_timer = QTimer()
58
- self.connect_retry_timer.setInterval(CONNECT_RETRY_INTERVAL)
59
- self.connect_retry_timer.setSingleShot(True)
60
- self.connect_retry_timer.timeout.connect(self.connect)
61
-
62
- def connect(self):
63
- try:
64
- self.conn_man = JackConnectionManager()
65
- except JackConnectError:
66
- self.connect_retry_timer.start()
67
- else:
68
- self.conn_man.on_error(self.jack_error)
69
- self.conn_man.on_shutdown(self.jack_shutdown)
70
- self.conn_man.on_client_registration(self.jack_client_registration)
71
- self.conn_man.on_port_registration(self.jack_port_registration)
72
- self.synth = JackLiquidSFZ(self.tempfile)
73
- self.sig_jack_ready.emit(self.conn_man.samplerate)
74
- self.synth.start()
75
-
76
- def quit(self):
77
- if self.synth and hasattr(self.synth, 'quit'):
78
- self.synth.ports_ready = False
79
- self.synth.quit() # pylint: disable = no-member
80
- unlink(self.tempfile)
81
-
82
- # -----------------------------------------------------------------
83
- # JACK callbacks
84
-
85
- def jack_client_registration(self, client_name, action):
86
- if action and not self.synth.client_name and client_name.startswith(SYNTH_NAME):
87
- self.synth.client_name = client_name
88
-
89
- def jack_port_registration(self, port, action):
90
- if action and self.synth.client_name and port.name.startswith(self.synth.client_name + ':'):
91
- if port.is_input and port.is_midi:
92
- self.synth.input_port = port
93
- elif port.is_output and port.is_audio:
94
- self.synth.output_ports.append(port)
95
- if self.synth.input_port and len(self.synth.output_ports) == 2:
96
- self.synth.ports_ready = True
97
- self.sig_ports_complete.emit()
98
- if port.is_input:
99
- self.sig_sinks_changed.emit()
100
- else:
101
- self.sig_sources_changed.emit()
102
-
103
- def jack_error(self, error_message):
104
- logging.error('JACK ERROR: "%s"', error_message)
105
-
106
- def jack_shutdown(self):
107
- logging.warning('JACK is shutting down')
108
- self.sig_jack_down.emit()
109
-
110
- @pyqtSlot()
111
- def slot_jack_down(self):
112
- """
113
- Triggered by sig_jack_down emitted from another thread in "jack_shutdown"
114
- """
115
- self.conn_man.close()
116
- self.conn_man = None
117
- self.connect_retry_timer.start()
118
-
119
- # -----------------------------------------------------------------
120
- # Source / sink management
121
-
122
- @pyqtSlot()
123
- def slot_ports_complete(self):
124
- """
125
- Triggered by sig_ports_complete, emitted in "jack_port_registration".
126
- """
127
- self.connect_midi_source()
128
- self.connect_audio_sinks()
129
-
130
- @pyqtSlot(str)
131
- def slot_midi_src_selected(self, value):
132
- """
133
- Triggered from combo box selection
134
- """
135
- self.midi_src = value
136
-
137
- @pyqtSlot(str)
138
- def slot_audio_sink_selected(self, value):
139
- """
140
- Triggered from combo box selection
141
- """
142
- self.audio_sink = value
143
-
144
- @pyqtSlot()
145
- def slot_sources_changed(self):
146
- """
147
- Triggered by sig_sources_changed, emitted in "jack_port_registration".
148
- """
149
- if self.synth.ports_ready:
150
- self.connect_midi_source()
151
-
152
- @pyqtSlot()
153
- def slot_sinks_changed(self):
154
- """
155
- Triggered by sig_sinks_changed, emitted in "jack_port_registration".
156
- """
157
- if self.synth.ports_ready:
158
- self.connect_audio_sinks()
159
-
160
- @property
161
- def midi_src(self):
162
- return get_setting(KEY_MIDI_SOURCE)
163
-
164
- @midi_src.setter
165
- def midi_src(self, value):
166
- set_setting(KEY_MIDI_SOURCE, value)
167
- self.connect_midi_source()
168
-
169
- @property
170
- def audio_sink(self):
171
- return get_setting(KEY_AUDIO_SINK)
172
-
173
- @audio_sink.setter
174
- def audio_sink(self, value):
175
- set_setting(KEY_AUDIO_SINK, value)
176
- self.connect_audio_sinks()
177
-
178
- def connect_midi_source(self):
179
- if self.synth and self.synth.ports_ready:
180
- # Look for source port if midi_src has a str value:
181
- if self.midi_src:
182
- src_port = self.conn_man.get_port_by_name(self.midi_src)
183
- # No need to disconnect / reconnect if they are equal:
184
- if src_port == self.synth.connected_midi_src_port:
185
- return
186
- else:
187
- src_port = None
188
- # Disconnect existing:
189
- if self.synth.connected_midi_src_port:
190
- self.conn_man.disconnect(self.synth.connected_midi_src_port, self.synth.input_port)
191
- # Connect if midi_src has a str value:
192
- if src_port:
193
- self.conn_man.connect(src_port, self.synth.input_port)
194
- self.sig_midi_connected.emit(src_port.name)
195
- # Update connected port (may be none):
196
- self.synth.connected_midi_src_port = src_port
197
-
198
- def connect_audio_sinks(self):
199
- if self.synth and self.synth.ports_ready:
200
- # Look for target ports if audio_sink has a str value:
201
- if self.audio_sink:
202
- tgt_ports = [ port for port in self.conn_man.get_client_ports(self.audio_sink)
203
- if port.is_audio and port.is_input ]
204
- # No need to disconnect / reconnect if they are equal:
205
- if tgt_ports == self.synth.connected_audio_sink_ports:
206
- return
207
- else:
208
- tgt_ports = []
209
- # Disconnect existing:
210
- if self.synth.connected_audio_sink_ports:
211
- for src, tgt in zip(
212
- self.synth.output_ports, self.synth.connected_audio_sink_ports):
213
- self.conn_man.disconnect(src, tgt)
214
- # Connect if audio_sink has a str value:
215
- if tgt_ports:
216
- for src, tgt in zip(self.synth.output_ports, tgt_ports):
217
- self.conn_man.connect(src, tgt)
218
- # Update connected ports (may be none):
219
- self.synth.connected_audio_sink_ports = tgt_ports
220
-
221
- def load_kit(self, kit):
222
- with open(self.tempfile, 'w', encoding = 'utf-8') as fob:
223
- kit.write(fob)
224
- self.synth.load(self.tempfile) # pylint: disable = no-member
225
-
226
-
227
- class JackLiquidSFZ(LiquidSFZ):
228
- """
229
- Wraps a LiquidSFZ instance in order to hold references to jacklib ports created
230
- by JackConnectionManager.
231
- """
232
-
233
- def __init__(self, filename):
234
- self.client_name = None
235
- self.ports_ready = False
236
- self.input_port = None
237
- self.output_ports = []
238
- self.connected_midi_src_port = None
239
- self.connected_audio_sink_ports = []
240
- super().__init__(filename, defer_start = True)
241
-
242
-
243
- # end kitbash/kitbash/jack_audio.py
File without changes
File without changes
File without changes
File without changes
File without changes