class FullscreenMenu(QMenu): def __init__(self, main_win, fullscreen_mngr): super().__init__(parent=main_win) self.fullscreen_mngr = fullscreen_mngr self.main_win = main_win self.setTitle("Fullscreen") self.setIcon(icons.get("fullscreen_menu_bttn")) self.qguiapp = QApplication.instance() self.action_group = QActionGroup(self) self.stop_fs_action = StopFullscreenAction( parent=self, fullscreen_mngr=self.fullscreen_mngr ) self.qscreens = None self.refresh_items() self.fullscreen_mngr.fullscreenstarted.connect(self.on_fullscreenstarted) self.fullscreen_mngr.fullscreenstopped.connect(self.on_fullscreenstopped) def refresh_items(self): # Clear action group for action in self.action_group.actions(): self.action_group.removeAction(action) del action # Add qscreen actions to group this_qscreen = get_qscreen_at(self.main_win) primary_qscreen = self.qguiapp.primaryScreen() sorted_qscreens = sorted(self.qguiapp.screens(), key=lambda s: s.name()) for qscreen in sorted_qscreens: is_primary = qscreen == primary_qscreen is_this_screen = qscreen == this_qscreen action = StartFullscreenAction( qscreen=qscreen, fullscreen_mngr=self.fullscreen_mngr, is_primary=is_primary, is_this_screen=is_this_screen, main_win=self.main_win, ) action.setCheckable(True) action.setIcon(icons.get("display_screen")) self.action_group.addAction(action) self.action_group.addAction(self.stop_fs_action) self.addActions(self.action_group.actions()) def on_menu_aboutToShow(self): self.setChecked(self.fullscreen_mngr.is_fullscreen()) @pyqtSlot(QAction) def on_fullscreenstarted(self, action): self.stop_fs_action.setEnabled(True) @pyqtSlot() def on_fullscreenstopped(self): self.stop_fs_action.setEnabled(False) def on_aboutToShow(self): self.refresh_items()
class ItemHistoryMenu(ItemHistory): """ Class for storing the history of items added to a QMenu """ def __init__(self, fileName: str, maxSize: int, menu: Any, callbackMethod: Any): ItemHistory.__init__(self, fileName, maxSize) self.menu = menu self.callbackMethod = callbackMethod items = self.tree.getElementsByTagName("item") self.actionGroup = QActionGroup(None) for node in items: text = self._getText(node) # type: str a = self.menu.addAction(text) # type: Any self.actionGroup.addAction(a) self.actionGroup.triggered.connect(self.actionTriggered) def _removeLastItem(self) -> None: actionList = self.actionGroup.actions() if actionList: a = actionList[-1] self.menu.removeAction(a) self.actionGroup.removeAction(a) def _insertItem(self, index: int, item: str) -> None: actionList = self.actionGroup.actions() actions = [] # type: List[str] for a in actionList: actions.append(a.text()) self.actionGroup.removeAction(a) self.menu.clear() actions = [item] + actions for action in actions: a = self.menu.addAction(action) self.actionGroup.addAction(a) self.actionGroup.triggered.connect(self.actionTriggered) def actionTriggered(self, action: QAction) -> None: self.callbackMethod(action.text())
class WMain(QMainWindow): def __init__(self): super().__init__() self.ctrl = QApplication.instance().ctrl self.ctrl.switch_language.connect(self.on_switch_language) self.ctrl.switch_configuration.connect(self.on_switch_configuration) self.paras = self.ctrl.paras self.config = GuiConf() self.init_ui() self.on_switch_language() self.on_switch_configuration() def init_ui(self): self.create_menus() self.tabframe = TabbedFrame(self) self.setCentralWidget(self.tabframe) self.resize(self.ctrl.config.window_width(), self.ctrl.config.window_height()) ####### menu bar ###################################### def create_menus(self): self.menubar = self.menuBar() self.menubar.setNativeMenuBar(False) self.menu_file = QMenu(_("File"), self) self.menubar.addMenu(self.menu_file) self.menu_open_config = QMenu(_("Load configuration"), self) self.menu_file.addMenu(self.menu_open_config) self.config_action_group = QActionGroup(self) self.config_action_group.triggered.connect( self.on_action_switch_config_triggered) self.menu_open_config.aboutToShow.connect( self.on_menu_open_config_about_to_show) self.action_save_configuration_as = QAction( _("Save configuration as..."), self) self.menu_file.addAction(self.action_save_configuration_as) self.action_save_configuration_as.triggered.connect( self.on_action_save_configuration_as_triggered) self.action_configurations = QAction(_("Configurations..."), self) self.menu_file.addAction(self.action_configurations) self.action_configurations.triggered.connect( self.on_action_configurations_triggered) self.menu_file.addSeparator() self.action_exit = QAction(_("Exit"), self) self.action_exit.setShortcut("Ctrl+Q") self.action_exit.triggered.connect(self.on_action_exit_triggered) self.menu_file.addAction(self.action_exit) self.menu_settings = self.create_menu_settings() self.menubar.addMenu(self.menu_settings) ####### menus ###################################### def create_menu_settings(self): menu_settings = QMenu(_("Preferences"), self) self.menu_language = self.create_menu_language() menu_settings.addMenu(self.menu_language) return menu_settings def create_menu_language(self): language_menu = QMenu(_("Language"), self) action_group = QActionGroup(self) action_group.triggered.connect( self.on_action_language_switch_triggered) iso_idx = self.ctrl.iso_lang() current_locale = self.ctrl.current_language() for locale in self.ctrl.locales(): short_locale = locale.split("-")[0] if short_locale in iso_idx: iso = iso_idx[short_locale] else: iso = {'nativeName': '-', 'name': '-'} action = QAction( "%s - %s [%s]" % (iso["nativeName"], iso["name"], locale), self) action.setCheckable(True) action.setData(locale) language_menu.addAction(action) action_group.addAction(action) if locale == current_locale: action.setChecked(True) return language_menu ####### functions ####################################### def on_switch_language(self, code=None): LOG.debug("Switch language: %s" % code) self.menu_file.setTitle(_("File")) self.action_exit.setText(_("Exit")) self.action_exit.setStatusTip(_("Exit application")) self.menu_open_config.setTitle(_("Load configuration")) self.action_save_configuration_as.setText( _("Save configuration as...")) self.action_configurations.setText(_("Configurations...")) self.menu_settings.setTitle(_("Preferences")) self.menu_language.setTitle(_("Language")) def on_switch_configuration(self, name=None): LOG.debug("Switch configuration: %s" % name) self.paras = self.ctrl.paras self.setWindowTitle(self.paras.configuration_name()) def on_menu_open_config_about_to_show(self): self.menu_open_config.clear() for child in self.config_action_group.children(): self.config_action_group.removeAction(child) current_name = Configurations.current_configuration_name() for name in Configurations.list_configurations(): action = QAction(name, self) action.setCheckable(True) action.setData(name) self.config_action_group.addAction(action) self.menu_open_config.addAction(action) if name == current_name: action.setChecked(True) def on_action_switch_config_triggered(self): action_group = self.sender() name = action_group.checkedAction().data() if self.tabframe.about_to_change(_("Switch configuration...")): self.ctrl.load_configuration(name) def on_action_save_configuration_as_triggered(self): text = _("Save configuration as...") if self.tabframe.about_to_change(text): dlg = QInputDialog(self) dlg.setInputMode(QInputDialog.TextInput) dlg.setWindowTitle(text) dlg.setLabelText(_("Configuration name:")) dlg.setTextValue(self.paras.configuration_name()) dlg.resize(300, 100) if dlg.exec_(): self.ctrl.save_configuration_as(dlg.textValue()) def on_action_configurations_triggered(self): ConfigurationsDialog(self).exec_() def on_action_language_switch_triggered(self): action_group = self.sender() locale = action_group.checkedAction().data() self.ctrl.set_language(locale) def on_action_exit_triggered(self): LOG.debug("action_exit_triggered") if self.tabframe.about_to_change(_("Closing application...")): qApp.quit() def closeEvent(self, event): LOG.debug("closeEvent was triggered") if self.tabframe.about_to_change(_("Closing application...")): event.accept() else: event.ignore() def close(self): LOG.debug("window closing") # self.ctrl.update_selector() self.ctrl.config.set_window_height(self.height()) self.ctrl.config.set_window_width(self.width()) self.ctrl.config.set_last_configuration_name( self.paras.configuration_name()) self.ctrl.config.persist() self.tabframe.close()
class Window(Ui_MainWindow, QMainWindow): quitCountDown = pyqtSignal() def __init__(self, ctx, *args, **kwargs): """ Normally initialize the MainWindow and some attributes and slots """ super(Window, self).__init__(*args, **kwargs) self.setupUi(self) self.ctx = ctx # A flag to distinguish between pausing and first running # 1: first_run self.first_run = 1 self.profileActions = {} # A dict contains profiles self._profile_group = QActionGroup(self) self.init_menu() self.init_data() self.init_slots() self.init_buttons() self.init_profiles_for_menu() self.init_timer() def init_timer(self): """Initialize the timer and the TimeController""" # The status of continung(1) and pausing(-1) self.controlStatus = -1 self.timer = QTimer() # Initialize the timer self.controller = TimeController(self.ctx) self.timer.setInterval(1000) self.timer.timeout.connect(self.controller.runner.minus) self.quitCountDown.connect(self.controller.quit_) self.controller.timeChanged.connect(self.update) self.controller.clearOld.connect(self.clearOld) self.controller.circleChanged.connect(self.updateLCD) self.controller.started.connect(self.timer.start) self.controller.finished.connect(self.reset) # After a whole counting down period, we say you eat a tomato. self.controller.eatTomato.connect(self.eat) self.controller.timerStart.connect(self.timer.start) self.controller.timerTempStop.connect(self.timer.stop) def init_data(self): """Display the data in the profile on the main window""" # Circle: work-rest period # Every 2 work-rest period, there is a long_rest. self.reload() self.workPB.setValue(0) self.restPB.setValue(0) self.long_restPB.setValue(0) def init_menu(self): """Initialize the mojarity of the actions placed in the menu""" self.actions = MenuActions(self.ctx) self.actionAbout.triggered.connect(self.actions.showAbout) self.actionMinimum.triggered.connect(self.showMinimized) self.actionQuit.triggered.connect(self.quit_) self.actionOptions.triggered.connect(self.actions.showOptions) self.actionDonate.triggered.connect(lambda: self.actions.donate(self)) def init_buttons(self): """Initialize the buttons lay out on the main window""" self.quitButton.clicked.connect(self.quit_) self.optionButton.clicked.connect(self.actions.showOptions) self.controlButton.clicked.connect(self.control) self.stopButton.clicked.connect(self.stop) self.stopButton.setEnabled(False) self.counterButton.clicked.connect(self.count) def init_slots(self): """Initialize the labels, lcdnumbers, statusbar and menubar""" # Initializethe slots of labels # These three label display the work time, rest time and long rest time. # To make the user interface more simple and keep your attention focused # on the task. I use the label instead of extra buttons to realize the # function of setting the time period quickly. # Also, you can find a full-functioned setting panel in the preference # menu and you can save these as a profile to quick load it when you use # the app next time as well. self.workLabel.doubleClicked.connect(self.setWorkTime) self.restLabel.doubleClicked.connect(self.setRestTime) self.longRestLabel.doubleClicked.connect(self.setLongRestTime) # Initialize the slots of circle lcdnumber self.circleTimesLCD.doubleClicked.connect(self.setCircleTimes) #Initialize the slots of profile_settings_window self.ctx.settings_gui.profileRemoved.connect(self.removeProfile) self.ctx.settings_gui.newProfileSig.connect(self.addProfileActions) self.ctx.settings_gui.updateSig.connect( self.refreshSelectedActionAndData) def init_profiles_for_menu(self): """Initialize the profiles found in the directory profiles and display them in the preferences menu. So you can get a easy access to use your profile quickly""" # Init the profile according to the global setting: "lastProfile" # Add the found profiles into the preferences menu and add them into # the ActionGroup, so you can only select one profile at one time self.ctx.profile.reloadObj.reloadSig.connect(self.reload) self.loadProfileActions() def refreshSelectedActionAndData(self): self.reload() self.profileActions[self.ctx.profile.name].setChecked(True) def addOneProfileAction(self, name): # QAction(name, parent) action = QAction(name, self) action.setCheckable(True) # connected with the reload method provided by profile object action.triggered.connect( lambda: self.ctx.profile.reload(action.text())) return action def addProfileActions(self, names): for name in names: action = self.addOneProfileAction(name) self.profileActions[name] = action self._profile_group.addAction(action) self.menuPreferences.addAction(action) def loadProfileActions(self): self.addProfileActions(self.ctx.profileList) self.profileActions[self.ctx.profile.name].setChecked(True) def removeProfile(self, name): try: action = self.profileActions.pop(name) except KeyError: pass else: self._profile_group.removeAction(action) self.menuPreferences.removeAction(action) sip.delete(action) def reload(self): """ When reloadSig triggered, this reload method will update the data shown on the main window. """ self.workCD.setText(self.ctx.profile["work"]) self.restCD.setText(self.ctx.profile["rest"]) self.long_restCD.setText(self.ctx.profile["long_rest"]) self.circleTimesLCD.display(self.ctx.profile["circle_times"]) if self.ctx.log4p.query(f"fatal:{self.ctx.profile.name}:"): self.controlButton.setEnabled(False) else: self.controlButton.setEnabled(True) def _set(self, name, attr, value): if value: self.ctx.profile[attr] = value if not self.ctx.inspector.check(value, attr): self.ctx.log4p.remove(f"fatal:{name}:{attr}") if self.ctx.log4p.query(f"fatal:{name}:") is None: self.controlButton.setEnabled(True) self.ctx.ask_dialog.close() def setWorkTime(self): f = lambda value: self._set(self.ctx.profile.name, "work", value) self.ctx.ask_dialog.replySig.connect(f) self.ctx.ask_dialog.ask("work", self.ctx.profile["work"]) self.workCD.setText(self.ctx.profile["work"]) self.workPB.setValue(0) self.ctx.ask_dialog.replySig.disconnect(f) def setRestTime(self): f = lambda value: self._set(self.ctx.profile.name, "rest", value) self.ctx.ask_dialog.replySig.connect(f) self.ctx.ask_dialog.ask("rest", self.ctx.profile["rest"]) self.restCD.setText(self.ctx.profile["rest"]) self.restPB.setValue(0) self.ctx.ask_dialog.replySig.disconnect(f) def setLongRestTime(self): f = lambda value: self._set(self.ctx.profile.name, "long_rest", value) self.ctx.ask_dialog.replySig.connect(f) self.ctx.ask_dialog.ask("long-resting", self.ctx.profile["long_rest"]) self.long_restCD.setText(self.ctx.profile["long_rest"]) self.long_restPB.setValue(0) self.ctx.ask_dialog.replySig.disconnect(f) def setCircleTimes(self): f = lambda value: self._set(self.ctx.profile.name, "circle_times", value) self.ctx.ask_dialog.replySig.connect(f) self.ctx.ask_dialog.ask("circle_times", self.ctx.profile["circle_times"]) self.circleTimesLCD.display(self.ctx.profile["circle_times"]) self.ctx.ask_dialog.replySig.disconnect(f) def reset(self): self.timer.stop() self.controlButton.setIcon(self.ctx.icontinue) self.stopButton.setEnabled(False) self.init_data() self.first_run = 1 self.controlStatus = -1 def eat(self): self.stopButton.setEnabled(False) self.ctx.global_setting["count"] += 1 self.statusBar.showMessage("You have eaten a tomato just now!", 2000) def quit_(self): """Quit the app""" self.ctx.profile.save() self.ctx.global_setting[ "lastProfile"] = self._profile_group.checkedAction().text() self.ctx.saveGlobal() self.ctx.app.quit() def count(self): self.statusBar.showMessage( "You have eaten {} tomatoes by now".format( self.ctx.global_setting["count"]), 1500) def update(self, type_, pc, time_gen): pb = type_ + "PB" label = type_ + "CD" getattr(self, label).setText("".join(time_gen)) getattr(self, pb).setValue(100 - pc) def updateLCD(self): num = self.circleTimesLCD.intValue() - 1 self.circleTimesLCD.display(num) def clearOld(self, type_): if type_ == "": type_ = "work" getattr(self, type_ + "PB").setValue(0) getattr(self, type_ + "CD").setText('0s') def control(self): self.controlStatus = -self.controlStatus if self.controlStatus == 1: # pause counting down self.controlButton.setIcon(self.ctx.ipause) self.statusBar.showMessage("continue", 1000) if self.first_run: self.first_run = 0 self.stopButton.setEnabled(True) self.statusBar.showMessage("preparing...", 2000) # Initialize the controller's counting down related data, # in order to keep the controller receiving # the newest profile data. self.controller.reset() self.controller.start() else: # continue counting down self.timer.start() else: self.controlButton.setIcon(self.ctx.icontinue) self.statusBar.showMessage("paused") self.timer.stop() def stop(self): """Quit counting down but not quit the app""" self.quitCountDown.emit() self.statusBar.showMessage("stopped", 1000)
class Gui(QMainWindow, Ui_MainWindow): NOTEON = 0x9 NOTEOFF = 0x8 MIDICTRL = 11 GREEN = ("#cell_frame { border: 0px; border-radius: 10px; " "background-color: rgb(125,242,0);}") BLUE = ("#cell_frame { border: 0px; border-radius: 10px; " "background-color: rgb(0, 130, 240);}") RED = ("#cell_frame { border: 0px; border-radius: 10px; " "background-color: rgb(255, 21, 65);}") AMBER = ("#cell_frame { border: 0px; border-radius: 10px; " "background-color: rgb(255, 102, 0);}") PURPLE = ("#cell_frame { border: 0px; border-radius: 10px; " "background-color: rgb(130, 0, 240);}") DEFAULT = ("#cell_frame { border: 0px; border-radius: 10px; " "background-color: rgb(217, 217, 217);}") RECORD_BLINK = ("QPushButton {background-color: rgb(255, 255, 255);}" "QPushButton:pressed {background-color: " "rgb(98, 98, 98);}") RECORD_DEFAULT = ("QPushButton {background-color: rgb(0, 0, 0);}" "QPushButton:pressed {background-color: " "rgb(98, 98, 98);}") STATE_COLORS = {Clip.STOP: RED, Clip.STARTING: GREEN, Clip.START: GREEN, Clip.STOPPING: RED, Clip.PREPARE_RECORD: AMBER, Clip.RECORDING: AMBER} STATE_BLINK = {Clip.STOP: False, Clip.STARTING: True, Clip.START: False, Clip.STOPPING: True, Clip.PREPARE_RECORD: True, Clip.RECORDING: False} BLINK_DURATION = 200 PROGRESS_PERIOD = 300 ADD_PORT_LABEL = 'Add new Port...' updateUi = pyqtSignal() readQueueIn = pyqtSignal() updatePorts = pyqtSignal() songLoad = pyqtSignal() def __init__(self, song, jack_client): QObject.__init__(self) super(Gui, self).__init__() self._jack_client = jack_client self.setupUi(self) self.clip_volume.knobRadius = 3 self.is_learn_device_mode = False self.queue_out, self.queue_in = Queue(), Queue() self.updateUi.connect(self.update) self.readQueueIn.connect(self.readQueue) self.current_vol_block = 0 self.last_clip = None # Load devices self.deviceGroup = QActionGroup(self.menuDevice) self.devices = [] device_settings = QSettings('superboucle', 'devices') if ((device_settings.contains('devices') and device_settings.value('devices'))): for raw_device in device_settings.value('devices'): self.devices.append(Device(pickle.loads(raw_device))) else: self.devices.append(Device({'name': 'No Device',})) self.updateDevices() self.deviceGroup.triggered.connect(self.onDeviceSelect) self.settings = QSettings('superboucle', 'session') # Qsetting appear to serialize empty lists as @QInvalid # which is then read as None :( # Load playlist self.playlist = self.settings.value('playlist', []) or [] # Load paths self.paths_used = self.settings.value('paths_used', {}) self.auto_connect = self.settings.value('auto_connect', 'true') == "true" # Load song self.port_by_name = {} self.initUI(song) self.actionNew.triggered.connect(self.onActionNew) self.actionOpen.triggered.connect(self.onActionOpen) self.actionSave.triggered.connect(self.onActionSave) self.actionSave_As.triggered.connect(self.onActionSaveAs) self.actionAdd_Device.triggered.connect(self.onAddDevice) self.actionManage_Devices.triggered.connect(self.onManageDevice) self.actionPlaylist_Editor.triggered.connect(self.onPlaylistEditor) self.actionScene_Manager.triggered.connect(self.onSceneManager) self.actionPort_Manager.triggered.connect(self.onPortManager) self.actionFullScreen.triggered.connect(self.onActionFullScreen) self.master_volume.valueChanged.connect(self.onMasterVolumeChange) self.bpm.valueChanged.connect(self.onBpmChange) self.beat_per_bar.valueChanged.connect(self.onBeatPerBarChange) self.rewindButton.clicked.connect(self.onRewindClicked) self.playButton.clicked.connect(self._jack_client.transport_start) self.pauseButton.clicked.connect(self._jack_client.transport_stop) self.gotoButton.clicked.connect(self.onGotoClicked) self.recordButton.clicked.connect(self.onRecord) self.clip_name.textChanged.connect(self.onClipNameChange) self.clip_volume.valueChanged.connect(self.onClipVolumeChange) self.beat_diviser.valueChanged.connect(self.onBeatDiviserChange) self.output.activated.connect(self.onOutputChange) self.mute_group.valueChanged.connect(self.onMuteGroupChange) self.frame_offset.valueChanged.connect(self.onFrameOffsetChange) self.beat_offset.valueChanged.connect(self.onBeatOffsetChange) self.revertButton.clicked.connect(self.onRevertClip) self.normalizeButton.clicked.connect(self.onNormalizeClip) self.exportButton.clicked.connect(self.onExportClip) self.deleteButton.clicked.connect(self.onDeleteClipClicked) self.blktimer = QTimer() self.blktimer.state = False self.blktimer.timeout.connect(self.toggleBlinkButton) self.blktimer.start(self.BLINK_DURATION) self.disptimer = QTimer() self.disptimer.start(self.PROGRESS_PERIOD) self.disptimer.timeout.connect(self.updateProgress) self._jack_client.set_timebase_callback(self.timebase_callback) self.show() def initUI(self, song): # remove old buttons self.btn_matrix = [[None for y in range(song.height)] for x in range(song.width)] self.state_matrix = [[-1 for y in range(song.height)] for x in range(song.width)] for i in reversed(range(self.gridLayout.count())): self.gridLayout.itemAt(i).widget().close() self.gridLayout.itemAt(i).widget().setParent(None) # first pass without removing old ports self.updateJackPorts(song, remove_ports=False) self.song = song # second pass with removing self.updateJackPorts(song, remove_ports=True) self.frame_clip.setEnabled(False) self.output.clear() self.output.addItems(song.outputsPorts) self.output.addItem(Gui.ADD_PORT_LABEL) self.master_volume.setValue(song.volume * 256) self.bpm.setValue(song.bpm) self.beat_per_bar.setValue(song.beat_per_bar) for x in range(song.width): for y in range(song.height): clip = song.clips_matrix[x][y] cell = Cell(self, clip, x, y) self.btn_matrix[x][y] = cell self.gridLayout.addWidget(cell, y, x) # send init command for init_cmd in self.device.init_command: self.queue_out.put(init_cmd) self.setWindowTitle("Super Boucle - {}" .format(song.file_name or "Empty Song")) if self.song.initial_scene in self.song.scenes: self.song.loadScene(self.song.initial_scene) self.update() self.songLoad.emit() def openSongFromDisk(self, file_name): self._jack_client.transport_stop() self._jack_client.transport_locate(0) self.setEnabled(False) message = QMessageBox(self) message.setWindowTitle("Loading ....") message.setText("Reading Files, please wait ...") message.show() self.initUI(load_song_from_file(file_name)) message.close() self.setEnabled(True) def closeEvent(self, event): device_settings = QSettings('superboucle', 'devices') device_settings.setValue('devices', [pickle.dumps(x.mapping) for x in self.devices]) self.settings.setValue('playlist', self.playlist) self.settings.setValue('paths_used', self.paths_used) self.settings.setValue('auto_connect', self.auto_connect) def onStartStopClicked(self): clip = self.sender().parent().parent().clip self.startStop(clip.x, clip.y) def startStop(self, x, y): clip = self.btn_matrix[x][y].clip if clip is None: return if self.song.is_record: self.song.is_record = False self.updateRecordBtn() # calculate buffer size state, position = self._jack_client.transport_query() bps = position['beats_per_minute'] / 60 fps = position['frame_rate'] size = int((1 / bps) * clip.beat_diviser * fps) self.song.init_record_buffer(clip, 2, size, fps) # set frame offset based on jack block size clip.frame_offset = self._jack_client.blocksize clip.state = Clip.PREPARE_RECORD self.recordButton.setStyleSheet(self.RECORD_DEFAULT) else: self.song.toggle(clip.x, clip.y) self.update() def onEdit(self): self.last_clip = self.sender().parent().parent().clip if self.last_clip: self.frame_clip.setEnabled(True) self.clip_name.setText(self.last_clip.name) self.frame_offset.setValue(self.last_clip.frame_offset) self.beat_offset.setValue(self.last_clip.beat_offset) self.beat_diviser.setValue(self.last_clip.beat_diviser) self.output.setCurrentText(self.last_clip.output) self.mute_group.setValue(self.last_clip.mute_group) self.clip_volume.setValue(self.last_clip.volume * 256) state, position = self._jack_client.transport_query() fps = position['frame_rate'] bps = self.bpm.value() / 60 if self.bpm.value() and fps: size_in_beat = (bps / fps) * self.song.length(self.last_clip) else: size_in_beat = "No BPM info" clip_description = ("Size in sample : %s\nSize in beat : %s" % (self.song.length(self.last_clip), round(size_in_beat, 1))) self.clip_description.setText(clip_description) def onAddClipClicked(self): cell = self.sender().parent().parent() if QApplication.keyboardModifiers() == Qt.ControlModifier: cell.setClip(cell.openClip()) else: AddClipDialog(self, cell) def onRevertClip(self): if self.last_clip and self.last_clip.audio_file: audio_file = self.last_clip.audio_file self.song.data[audio_file] = self.song.data[audio_file][::-1] def onNormalizeClip(self): if self.last_clip and self.last_clip.audio_file: audio_file = self.last_clip.audio_file absolute_val = np.absolute(self.song.data[audio_file]) current_level = np.ndarray.max(absolute_val) self.song.data[audio_file][:] *= (1 / current_level) def onExportClip(self): if self.last_clip and self.last_clip.audio_file: audio_file = self.last_clip.audio_file file_name, a = self.getSaveFileName( 'Export Clip : %s' % self.last_clip.name, 'WAVE (*.wav)') if file_name: file_name = verify_ext(file_name, 'wav') sf.write(self.song.data[audio_file], file_name, self.song.samplerate[audio_file], subtype=sf.default_subtype('WAV'), format='WAV') def onDeleteClipClicked(self): if self.last_clip: response = QMessageBox.question(self, "Delete Clip ?", ("Are you sure " "to delete the clip ?")) if response == QMessageBox.Yes: self.frame_clip.setEnabled(False) self.song.removeClip(self.last_clip) self.initUI(self.song) def onMasterVolumeChange(self): self.song.volume = (self.master_volume.value() / 256) def onBpmChange(self): self.song.bpm = self.bpm.value() def onBeatPerBarChange(self): self.song.beat_per_bar = self.beat_per_bar.value() def onGotoClicked(self): state, position = self._jack_client.transport_query() new_position = (position['beats_per_bar'] * (self.gotoTarget.value() - 1) * position['frame_rate'] * (60 / position['beats_per_minute'])) self._jack_client.transport_locate(int(round(new_position, 0))) def onRecord(self): self.song.is_record = not self.song.is_record self.updateRecordBtn() def updateRecordBtn(self): if not self.song.is_record: self.recordButton.setStyleSheet(self.RECORD_DEFAULT) if self.device.record_btn: (msg_type, channel, pitch, velocity) = self.device.record_btn if self.song.is_record: color = self.device.blink_amber_vel else: color = self.device.black_vel self.queue_out.put(((msg_type << 4) + channel, pitch, color)) def onRewindClicked(self): self._jack_client.transport_locate(0) def onClipNameChange(self): self.last_clip.name = self.clip_name.text() cell = self.btn_matrix[self.last_clip.x][self.last_clip.y] cell.clip_name.setText(self.last_clip.name) def onClipVolumeChange(self): self.last_clip.volume = (self.clip_volume.value() / 256) def onBeatDiviserChange(self): self.last_clip.beat_diviser = self.beat_diviser.value() def onOutputChange(self): new_port = self.output.currentText() if new_port == Gui.ADD_PORT_LABEL: AddPortDialog(self) else: self.last_clip.output = new_port def addPort(self, name): self.song.outputsPorts.add(name) self.updateJackPorts(self.song) if self.output.findText(name) == -1: self.output.insertItem(self.output.count() - 1, name) if self.last_clip: self.last_clip.output = name self.output.setCurrentText(name) def removePort(self, name): if name != Clip.DEFAULT_OUTPUT: self.song.outputsPorts.remove(name) for c in self.song.clips: if c.output == name: c.output = Clip.DEFAULT_OUTPUT self.updateJackPorts(self.song) self.output.removeItem(self.output.findText(name)) if self.last_clip: self.output.setCurrentText(self.last_clip.output) def updateJackPorts(self, song, remove_ports=True): '''Update jack port based on clip output settings update dict containing ports with shortname as key''' current_ports = set() for port in self._jack_client.outports: current_ports.add(port.shortname) wanted_ports = set() for port_basename in song.outputsPorts: for ch in Song.CHANNEL_NAMES: port = Song.CHANNEL_NAME_PATTERN.format(port=port_basename, channel=ch) wanted_ports.add(port) # remove unwanted ports if remove_ports: port_to_remove = [] for port in self._jack_client.outports: if port.shortname not in wanted_ports: current_ports.remove(port.shortname) port_to_remove.append(port) for port in port_to_remove: port.unregister() # create new ports for new_port_name in wanted_ports - current_ports: self._jack_client.outports.register(new_port_name) self.port_by_name = {port.shortname: port for port in self._jack_client.outports} self.updatePorts.emit() def onMuteGroupChange(self): self.last_clip.mute_group = self.mute_group.value() def onFrameOffsetChange(self): self.last_clip.frame_offset = self.frame_offset.value() def onBeatOffsetChange(self): self.last_clip.beat_offset = self.beat_offset.value() def onActionNew(self): NewSongDialog(self) def getOpenFileName(self, title, file_type, parent=None, dialog=QFileDialog.getOpenFileName): path = self.paths_used.get(file_type, expanduser('~')) file_name, a = dialog(parent or self, title, path, file_type) if a and file_name: if isinstance(file_name, list): self.paths_used[file_type] = dirname(file_name[0]) else: self.paths_used[file_type] = dirname(file_name) return file_name, a def getSaveFileName(self, *args): return self.getOpenFileName(*args, dialog=QFileDialog.getSaveFileName) def onActionOpen(self): file_name, a = self.getOpenFileName('Open Song', 'Super Boucle Song (*.sbs)') if a and file_name: self.openSongFromDisk(file_name) def onActionSave(self): if self.song.file_name: self.song.save() else: self.onActionSaveAs() def onActionSaveAs(self): file_name, a = self.getSaveFileName('Save Song', 'Super Boucle Song (*.sbs)') if file_name: file_name = verify_ext(file_name, 'sbs') self.song.file_name = file_name self.song.save() print("File saved to : {}".format(self.song.file_name)) def onAddDevice(self): self.learn_device = LearnDialog(self, self.addDevice) self.is_learn_device_mode = True def onManageDevice(self): ManageDialog(self) def onPlaylistEditor(self): PlaylistDialog(self) def onSceneManager(self): SceneManager(self) def onPortManager(self): PortManager(self) def onActionFullScreen(self): if self.isFullScreen(): self.showNormal() else: self.showFullScreen() self.show() def update(self): for x in range(len(self.song.clips_matrix)): line = self.song.clips_matrix[x] for y in range(len(line)): clp = line[y] if clp is None: state = None else: state = clp.state if state != self.state_matrix[x][y]: if clp: self.btn_matrix[x][y].setColor(state) try: self.queue_out.put(self.device.generateNote(x, y, state)) except IndexError: # print("No cell associated to %s x %s" # % (clp.x, clp.y)) pass self.state_matrix[x][y] = state def redraw(self): self.state_matrix = [[-1 for x in range(self.song.height)] for x in range(self.song.width)] self.update() def readQueue(self): try: while True: note = self.queue_in.get(block=False) if len(note) == 3: status, pitch, vel = struct.unpack('3B', note) channel = status & 0xF msg_type = status >> 4 self.processNote(msg_type, channel, pitch, vel) # else: # print("Invalid message length") except Empty: pass def processNote(self, msg_type, channel, pitch, vel): btn_id = (msg_type, channel, pitch, vel) btn_id_vel = (msg_type, channel, pitch, -1) ctrl_key = (msg_type, channel, pitch) # master volume if ctrl_key == self.device.master_volume_ctrl: self.song.master_volume = vel / 127 (self.master_volume .setValue(self.song.master_volume * 256)) elif self.device.play_btn in [btn_id, btn_id_vel]: self._jack_client.transport_start() elif self.device.pause_btn in [btn_id, btn_id_vel]: self._jack_client.transport_stop() elif self.device.rewind_btn in [btn_id, btn_id_vel]: self.onRewindClicked() elif self.device.goto_btn in [btn_id, btn_id_vel]: self.onGotoClicked() elif self.device.record_btn in [btn_id, btn_id_vel]: self.onRecord() elif ctrl_key in self.device.ctrls: try: ctrl_index = self.device.ctrls.index(ctrl_key) clip = (self.song.clips_matrix [ctrl_index] [self.current_vol_block]) if clip: clip.volume = vel / 127 if self.last_clip == clip: self.clip_volume.setValue(self.last_clip.volume * 256) except KeyError: pass elif (btn_id in self.device.scene_buttons or btn_id_vel in self.device.scene_buttons): try: scene_id = self.device.scene_buttons.index(btn_id) except ValueError: scene_id = self.device.scene_buttons.index(btn_id_vel) try: self.song.loadSceneId(scene_id) self.update() except IndexError: print('cannot load scene {} - there are only {} scenes.' ''.format(scene_id, len(self.song.scenes))) elif (btn_id in self.device.block_buttons or btn_id_vel in self.device.block_buttons): try: self.current_vol_block = ( self.device.block_buttons.index(btn_id)) except ValueError: self.current_vol_block = ( self.device.block_buttons.index(btn_id_vel)) for i in range(len(self.device.block_buttons)): (a, b_channel, b_pitch, b) = self.device.block_buttons[i] if i == self.current_vol_block: color = self.device.red_vel else: color = self.device.black_vel self.queue_out.put(((self.NOTEON << 4) + b_channel, b_pitch, color)) else: x, y = -1, -1 try: x, y = self.device.getXY(btn_id) except IndexError: pass except KeyError: try: x, y = self.device.getXY(btn_id_vel) except KeyError: pass if (x >= 0 and y >= 0): self.startStop(x, y) def toggleBlinkButton(self): for line in self.btn_matrix: for btn in line: if btn.blink: if self.blktimer.state: btn.setStyleSheet(btn.color) else: btn.setStyleSheet(self.DEFAULT) if self.song.is_record: if self.blktimer.state: self.recordButton.setStyleSheet(self.RECORD_BLINK) else: self.recordButton.setStyleSheet(self.RECORD_DEFAULT) self.blktimer.state = not self.blktimer.state def updateProgress(self): state, pos = self._jack_client.transport_query() if 'bar' in pos: bbt = "%d|%d|%03d" % (pos['bar'], pos['beat'], pos['tick']) else: bbt = "-|-|-" seconds = int(pos['frame'] / pos['frame_rate']) (minutes, second) = divmod(seconds, 60) (hour, minute) = divmod(minutes, 60) time = "%d:%02d:%02d" % (hour, minute, second) self.bbtLabel.setText("%s\n%s" % (bbt, time)) for line in self.btn_matrix: for btn in line: if btn.clip and btn.clip.audio_file: value = ((btn.clip.last_offset / self.song.length(btn.clip)) * 97) btn.clip_position.setValue(value) btn.clip_position.repaint() def updateDevices(self): for action in self.deviceGroup.actions(): self.deviceGroup.removeAction(action) self.menuDevice.removeAction(action) for device in self.devices: action = QAction(device.name, self.menuDevice) action.setCheckable(True) action.setData(device) self.menuDevice.addAction(action) self.deviceGroup.addAction(action) action.setChecked(True) self.device = device def addDevice(self, device): self.devices.append(device) self.updateDevices() self.is_learn_device_mode = False def onDeviceSelect(self): self.device = self.deviceGroup.checkedAction().data() if self.device: if self.device.init_command: for note in self.device.init_command: self.queue_out.put(note) self.redraw() def timebase_callback(self, state, nframes, pos, new_pos): if pos.frame_rate == 0: return None pos.valid = 0x10 pos.bar_start_tick = BAR_START_TICK pos.beats_per_bar = self.beat_per_bar.value() pos.beat_type = BEAT_TYPE pos.ticks_per_beat = TICKS_PER_BEAT pos.beats_per_minute = self.bpm.value() ticks_per_second = (pos.beats_per_minute * pos.ticks_per_beat) / 60 ticks = (ticks_per_second * pos.frame) / pos.frame_rate (beats, pos.tick) = divmod(int(round(ticks, 0)), int(round(pos.ticks_per_beat, 0))) (bar, beat) = divmod(beats, int(round(pos.beats_per_bar, 0))) (pos.bar, pos.beat) = (bar + 1, beat + 1) return None
class Gui(QMainWindow, Ui_MainWindow): NOTEON = 0x9 NOTEOFF = 0x8 MIDICTRL = 11 GREEN = ("#cell_frame { border: 0px; border-radius: 10px; " "background-color: rgb(125,242,0);}") BLUE = ("#cell_frame { border: 0px; border-radius: 10px; " "background-color: rgb(0, 130, 240);}") RED = ("#cell_frame { border: 0px; border-radius: 10px; " "background-color: rgb(255, 21, 65);}") AMBER = ("#cell_frame { border: 0px; border-radius: 10px; " "background-color: rgb(255, 102, 0);}") PURPLE = ("#cell_frame { border: 0px; border-radius: 10px; " "background-color: rgb(130, 0, 240);}") DEFAULT = ("#cell_frame { border: 0px; border-radius: 10px; " "background-color: rgb(217, 217, 217);}") RECORD_BLINK = ("QPushButton {background-color: rgb(255, 255, 255);}" "QPushButton:pressed {background-color: " "rgb(98, 98, 98);}") RECORD_DEFAULT = ("QPushButton {background-color: rgb(0, 0, 0);}" "QPushButton:pressed {background-color: " "rgb(98, 98, 98);}") STATE_COLORS = {Clip.STOP: RED, Clip.STARTING: GREEN, Clip.START: GREEN, Clip.STOPPING: RED, Clip.PREPARE_RECORD: AMBER, Clip.RECORDING: AMBER} STATE_BLINK = {Clip.STOP: False, Clip.STARTING: True, Clip.START: False, Clip.STOPPING: True, Clip.PREPARE_RECORD: True, Clip.RECORDING: False} BLINK_DURATION = 200 PROGRESS_PERIOD = 300 ADD_PORT_LABEL = 'Add new Port...' updateUi = pyqtSignal() readQueueIn = pyqtSignal() updatePorts = pyqtSignal() songLoad = pyqtSignal() def __init__(self, song, jack_client): QObject.__init__(self) super(Gui, self).__init__() self._jack_client = jack_client self.setupUi(self) self.clip_volume.knobRadius = 3 self.is_learn_device_mode = False self.queue_out, self.queue_in = Queue(), Queue() self.updateUi.connect(self.update) self.readQueueIn.connect(self.readQueue) self.current_vol_block = 0 self.last_clip = None # Load devices self.deviceGroup = QActionGroup(self.menuDevice) self.devices = [] device_settings = QSettings('superboucle', 'devices') if ((device_settings.contains('devices') and device_settings.value('devices'))): for raw_device in device_settings.value('devices'): self.devices.append(Device(pickle.loads(raw_device))) else: self.devices.append(Device({'name': 'No Device',})) self.updateDevices() self.deviceGroup.triggered.connect(self.onDeviceSelect) self.settings = QSettings('superboucle', 'session') # Qsetting appear to serialize empty lists as @QInvalid # which is then read as None :( # Load playlist self.playlist = self.settings.value('playlist', []) or [] # Load paths self.paths_used = self.settings.value('paths_used', {}) self.auto_connect = self.settings.value('auto_connect', 'true') == "true" # Load song self.port_by_name = {} self.initUI(song) self.actionNew.triggered.connect(self.onActionNew) self.actionOpen.triggered.connect(self.onActionOpen) self.actionSave.triggered.connect(self.onActionSave) self.actionSave_As.triggered.connect(self.onActionSaveAs) self.actionAdd_Device.triggered.connect(self.onAddDevice) self.actionManage_Devices.triggered.connect(self.onManageDevice) self.actionPlaylist_Editor.triggered.connect(self.onPlaylistEditor) self.actionScene_Manager.triggered.connect(self.onSceneManager) self.actionPort_Manager.triggered.connect(self.onPortManager) self.actionFullScreen.triggered.connect(self.onActionFullScreen) self.master_volume.valueChanged.connect(self.onMasterVolumeChange) self.bpm.valueChanged.connect(self.onBpmChange) self.beat_per_bar.valueChanged.connect(self.onBeatPerBarChange) self.rewindButton.clicked.connect(self.onRewindClicked) self.playButton.clicked.connect(self._jack_client.transport_start) self.pauseButton.clicked.connect(self._jack_client.transport_stop) self.gotoButton.clicked.connect(self.onGotoClicked) self.recordButton.clicked.connect(self.onRecord) self.clip_name.textChanged.connect(self.onClipNameChange) self.clip_volume.valueChanged.connect(self.onClipVolumeChange) self.beat_diviser.valueChanged.connect(self.onBeatDiviserChange) self.output.activated.connect(self.onOutputChange) self.mute_group.valueChanged.connect(self.onMuteGroupChange) self.frame_offset.valueChanged.connect(self.onFrameOffsetChange) self.beat_offset.valueChanged.connect(self.onBeatOffsetChange) self.revertButton.clicked.connect(self.onRevertClip) self.normalizeButton.clicked.connect(self.onNormalizeClip) self.exportButton.clicked.connect(self.onExportClip) self.deleteButton.clicked.connect(self.onDeleteClipClicked) self.blktimer = QTimer() self.blktimer.state = False self.blktimer.timeout.connect(self.toggleBlinkButton) self.blktimer.start(self.BLINK_DURATION) self.disptimer = QTimer() self.disptimer.start(self.PROGRESS_PERIOD) self.disptimer.timeout.connect(self.updateProgress) self._jack_client.set_timebase_callback(self.timebase_callback) self.show() def initUI(self, song): # remove old buttons self.btn_matrix = [[None for y in range(song.height)] for x in range(song.width)] self.state_matrix = [[-1 for y in range(song.height)] for x in range(song.width)] for i in reversed(range(self.gridLayout.count())): self.gridLayout.itemAt(i).widget().close() self.gridLayout.itemAt(i).widget().setParent(None) # first pass without removing old ports self.updateJackPorts(song, remove_ports=False) self.song = song # second pass with removing self.updateJackPorts(song, remove_ports=True) self.frame_clip.setEnabled(False) self.output.clear() self.output.addItems(song.outputsPorts) self.output.addItem(Gui.ADD_PORT_LABEL) self.master_volume.setValue(song.volume * 256) self.bpm.setValue(song.bpm) self.beat_per_bar.setValue(song.beat_per_bar) for x in range(song.width): for y in range(song.height): clip = song.clips_matrix[x][y] cell = Cell(self, clip, x, y) self.btn_matrix[x][y] = cell self.gridLayout.addWidget(cell, y, x) # send init command for init_cmd in self.device.init_command: self.queue_out.put(init_cmd) self.setWindowTitle("Super Boucle - {}" .format(song.file_name or "Empty Song")) if self.song.initial_scene in self.song.scenes: self.song.loadScene(self.song.initial_scene) self.update() self.songLoad.emit() timer = QTimer() timer.singleShot(1000,self.send_clip_state_feedback) def openSongFromDisk(self, file_name): self._jack_client.transport_stop() self._jack_client.transport_locate(0) self.setEnabled(False) message = QMessageBox(self) message.setWindowTitle("Loading ....") message.setText("Reading Files, please wait ...") message.show() self.initUI(load_song_from_file(file_name)) message.close() self.setEnabled(True) def closeEvent(self, event): device_settings = QSettings('superboucle', 'devices') device_settings.setValue('devices', [pickle.dumps(x.mapping) for x in self.devices]) self.settings.setValue('playlist', self.playlist) self.settings.setValue('paths_used', self.paths_used) self.settings.setValue('auto_connect', self.auto_connect) def onStartStopClicked(self): clip = self.sender().parent().parent().clip self.startStop(clip.x, clip.y) def startStop(self, x, y): clip = self.btn_matrix[x][y].clip if clip is None: return if self.song.is_record: self.song.is_record = False self.updateRecordBtn() # calculate buffer size state, position = self._jack_client.transport_query() bps = position['beats_per_minute'] / 60 fps = position['frame_rate'] size = (1 / bps) * clip.beat_diviser * fps self.song.init_record_buffer(clip, 2, size, fps) # set frame offset based on jack block size clip.frame_offset = self._jack_client.blocksize clip.state = Clip.PREPARE_RECORD self.recordButton.setStyleSheet(self.RECORD_DEFAULT) else: self.song.toggle(clip.x, clip.y) self.update() def onEdit(self): self.last_clip = self.sender().parent().parent().clip if self.last_clip: self.frame_clip.setEnabled(True) self.clip_name.setText(self.last_clip.name) self.frame_offset.setValue(self.last_clip.frame_offset) self.beat_offset.setValue(self.last_clip.beat_offset) self.beat_diviser.setValue(self.last_clip.beat_diviser) self.output.setCurrentText(self.last_clip.output) self.mute_group.setValue(self.last_clip.mute_group) self.clip_volume.setValue(self.last_clip.volume * 256) state, position = self._jack_client.transport_query() fps = position['frame_rate'] bps = self.bpm.value() / 60 if self.bpm.value() and fps: size_in_beat = (bps / fps) * self.song.length(self.last_clip) else: size_in_beat = "No BPM info" clip_description = ("Size in sample : %s\nSize in beat : %s" % (self.song.length(self.last_clip), round(size_in_beat, 1))) self.clip_description.setText(clip_description) def onAddClipClicked(self): cell = self.sender().parent().parent() if QApplication.keyboardModifiers() == Qt.ControlModifier: cell.setClip(cell.openClip()) else: AddClipDialog(self, cell) def onRevertClip(self): if self.last_clip and self.last_clip.audio_file: audio_file = self.last_clip.audio_file self.song.data[audio_file] = self.song.data[audio_file][::-1] def onNormalizeClip(self): if self.last_clip and self.last_clip.audio_file: audio_file = self.last_clip.audio_file absolute_val = np.absolute(self.song.data[audio_file]) current_level = np.ndarray.max(absolute_val) self.song.data[audio_file][:] *= (1 / current_level) def onExportClip(self): if self.last_clip and self.last_clip.audio_file: audio_file = self.last_clip.audio_file file_name, a = self.getSaveFileName( 'Export Clip : %s' % self.last_clip.name, 'WAVE (*.wav)') if file_name: file_name = verify_ext(file_name, 'wav') sf.write(self.song.data[audio_file], file_name, self.song.samplerate[audio_file], subtype=sf.default_subtype('WAV'), format='WAV') def onDeleteClipClicked(self): if self.last_clip: response = QMessageBox.question(self, "Delete Clip ?", ("Are you sure " "to delete the clip ?")) if response == QMessageBox.Yes: self.frame_clip.setEnabled(False) self.song.removeClip(self.last_clip) self.initUI(self.song) def onMasterVolumeChange(self): self.song.volume = (self.master_volume.value() / 256) def onBpmChange(self): self.song.bpm = self.bpm.value() def onBeatPerBarChange(self): self.song.beat_per_bar = self.beat_per_bar.value() def onGotoClicked(self): state, position = self._jack_client.transport_query() new_position = (position['beats_per_bar'] * (self.gotoTarget.value() - 1) * position['frame_rate'] * (60 / position['beats_per_minute'])) self._jack_client.transport_locate(int(round(new_position, 0))) def onRecord(self): self.song.is_record = not self.song.is_record self.updateRecordBtn() def updateRecordBtn(self): if not self.song.is_record: self.recordButton.setStyleSheet(self.RECORD_DEFAULT) if self.device.record_btn: (msg_type, channel, pitch, velocity) = self.device.record_btn if self.song.is_record: color = self.device.blink_amber_vel else: color = self.device.black_vel self.queue_out.put(((msg_type << 4) + channel, pitch, color)) def onRewindClicked(self): self._jack_client.transport_locate(0) def onClipNameChange(self): self.last_clip.name = self.clip_name.text() cell = self.btn_matrix[self.last_clip.x][self.last_clip.y] cell.clip_name.setText(self.last_clip.name) def onClipVolumeChange(self): self.last_clip.volume = (self.clip_volume.value() / 256) def onBeatDiviserChange(self): self.last_clip.beat_diviser = self.beat_diviser.value() def onOutputChange(self): new_port = self.output.currentText() if new_port == Gui.ADD_PORT_LABEL: AddPortDialog(self) else: self.last_clip.output = new_port def addPort(self, name): self.song.outputsPorts.add(name) self.updateJackPorts(self.song) if self.output.findText(name) == -1: self.output.insertItem(self.output.count() - 1, name) if self.last_clip: self.last_clip.output = name self.output.setCurrentText(name) def removePort(self, name): if name != Clip.DEFAULT_OUTPUT: self.song.outputsPorts.remove(name) for c in self.song.clips: if c.output == name: c.output = Clip.DEFAULT_OUTPUT self.updateJackPorts(self.song) self.output.removeItem(self.output.findText(name)) if self.last_clip: self.output.setCurrentText(self.last_clip.output) def updateJackPorts(self, song, remove_ports=True): '''Update jack port based on clip output settings update dict containing ports with shortname as key''' current_ports = set() for port in self._jack_client.outports: current_ports.add(port.shortname) wanted_ports = set() for port_basename in song.outputsPorts: for ch in Song.CHANNEL_NAMES: port = Song.CHANNEL_NAME_PATTERN.format(port=port_basename, channel=ch) wanted_ports.add(port) # remove unwanted ports if remove_ports: port_to_remove = [] for port in self._jack_client.outports: if port.shortname not in wanted_ports: current_ports.remove(port.shortname) port_to_remove.append(port) for port in port_to_remove: port.unregister() # create new ports for new_port_name in wanted_ports - current_ports: self._jack_client.outports.register(new_port_name) self.port_by_name = {port.shortname: port for port in self._jack_client.outports} self.updatePorts.emit() def onMuteGroupChange(self): self.last_clip.mute_group = self.mute_group.value() def onFrameOffsetChange(self): self.last_clip.frame_offset = self.frame_offset.value() def onBeatOffsetChange(self): self.last_clip.beat_offset = self.beat_offset.value() def onActionNew(self): NewSongDialog(self) def getOpenFileName(self, title, file_type, parent=None, dialog=QFileDialog.getOpenFileName): path = self.paths_used.get(file_type, expanduser('~')) file_name, a = dialog(parent or self, title, path, file_type) if a and file_name: if isinstance(file_name, list): self.paths_used[file_type] = dirname(file_name[0]) else: self.paths_used[file_type] = dirname(file_name) return file_name, a def getSaveFileName(self, *args): return self.getOpenFileName(*args, dialog=QFileDialog.getSaveFileName) def onActionOpen(self): file_name, a = self.getOpenFileName('Open Song', 'Super Boucle Song (*.sbs)') if a and file_name: self.openSongFromDisk(file_name) def onActionSave(self): if self.song.file_name: self.song.save() else: self.onActionSaveAs() def onActionSaveAs(self): file_name, a = self.getSaveFileName('Save Song', 'Super Boucle Song (*.sbs)') if file_name: file_name = verify_ext(file_name, 'sbs') self.song.file_name = file_name self.song.save() print("File saved to : {}".format(self.song.file_name)) def onAddDevice(self): self.learn_device = LearnDialog(self, self.addDevice) self.is_learn_device_mode = True def onManageDevice(self): ManageDialog(self) def onPlaylistEditor(self): PlaylistDialog(self) def onSceneManager(self): SceneManager(self) def onPortManager(self): PortManager(self) def onActionFullScreen(self): if self.isFullScreen(): self.showNormal() else: self.showFullScreen() self.show() def send_clip_state_feedback(self): for x in range(self.song.width): for y in range(self.song.height): clip = self.song.clips_matrix[x][y] state = clip.state if clip else None self._update_clip_state(x, y, state) def _update_clip_state(self, x, y, state): clip = self.song.clips_matrix[x][y] if clip: self.btn_matrix[x][y].setColor(state) try: self.queue_out.put(self.device.generateNote(x, y, state)) except IndexError: # print("No cell associated to %s x %s" # % (clp.x, clp.y)) pass self.state_matrix[x][y] = state def update(self): for x in range(len(self.song.clips_matrix)): line = self.song.clips_matrix[x] for y in range(len(line)): clp = line[y] if clp is None: state = None else: state = clp.state if state != self.state_matrix[x][y]: self._update_clip_state(x, y, state) def redraw(self): self.state_matrix = [[-1 for x in range(self.song.height)] for x in range(self.song.width)] self.update() def readQueue(self): try: while True: note = self.queue_in.get(block=False) if len(note) == 3: status, pitch, vel = struct.unpack('3B', note) channel = status & 0xF msg_type = status >> 4 self.processNote(msg_type, channel, pitch, vel) # else: # print("Invalid message length") except Empty: pass def processNote(self, msg_type, channel, pitch, vel): btn_id = (msg_type, channel, pitch, vel) btn_id_vel = (msg_type, channel, pitch, -1) ctrl_key = (msg_type, channel, pitch) # master volume if ctrl_key == self.device.master_volume_ctrl: self.song.master_volume = vel / 127 (self.master_volume .setValue(self.song.master_volume * 256)) elif self.device.play_btn in [btn_id, btn_id_vel]: self._jack_client.transport_start() elif self.device.pause_btn in [btn_id, btn_id_vel]: self._jack_client.transport_stop() elif self.device.rewind_btn in [btn_id, btn_id_vel]: self.onRewindClicked() elif self.device.goto_btn in [btn_id, btn_id_vel]: self.onGotoClicked() elif self.device.record_btn in [btn_id, btn_id_vel]: self.onRecord() elif ctrl_key in self.device.ctrls: try: ctrl_index = self.device.ctrls.index(ctrl_key) clip = (self.song.clips_matrix [ctrl_index] [self.current_vol_block]) if clip: clip.volume = vel / 127 if self.last_clip == clip: self.clip_volume.setValue(self.last_clip.volume * 256) except KeyError: pass elif (btn_id in self.device.scene_buttons or btn_id_vel in self.device.scene_buttons): try: scene_id = self.device.scene_buttons.index(btn_id) except ValueError: scene_id = self.device.scene_buttons.index(btn_id_vel) try: self.song.loadSceneId(scene_id) self.update() except IndexError: print('cannot load scene {} - there are only {} scenes.' ''.format(scene_id, len(self.song.scenes))) elif (btn_id in self.device.block_buttons or btn_id_vel in self.device.block_buttons): try: self.current_vol_block = ( self.device.block_buttons.index(btn_id)) except ValueError: self.current_vol_block = ( self.device.block_buttons.index(btn_id_vel)) for i in range(len(self.device.block_buttons)): (a, b_channel, b_pitch, b) = self.device.block_buttons[i] if i == self.current_vol_block: color = self.device.red_vel else: color = self.device.black_vel self.queue_out.put(((self.NOTEON << 4) + b_channel, b_pitch, color)) else: x, y = -1, -1 try: x, y = self.device.getXY(btn_id) except IndexError: pass except KeyError: try: x, y = self.device.getXY(btn_id_vel) except KeyError: pass if (x >= 0 and y >= 0): self.startStop(x, y) def toggleBlinkButton(self): for line in self.btn_matrix: for btn in line: if btn.blink: if self.blktimer.state: btn.setStyleSheet(btn.color) else: btn.setStyleSheet(self.DEFAULT) if self.song.is_record: if self.blktimer.state: self.recordButton.setStyleSheet(self.RECORD_BLINK) else: self.recordButton.setStyleSheet(self.RECORD_DEFAULT) self.blktimer.state = not self.blktimer.state def updateProgress(self): state, pos = self._jack_client.transport_query() if 'bar' in pos: bbt = "%d|%d|%03d" % (pos['bar'], pos['beat'], pos['tick']) else: bbt = "-|-|-" seconds = int(pos['frame'] / pos['frame_rate']) (minutes, second) = divmod(seconds, 60) (hour, minute) = divmod(minutes, 60) time = "%d:%02d:%02d" % (hour, minute, second) self.bbtLabel.setText("%s\n%s" % (bbt, time)) for line in self.btn_matrix: for btn in line: if btn.clip and btn.clip.audio_file: value = ((btn.clip.last_offset / self.song.length(btn.clip)) * 97) btn.clip_position.setValue(value) btn.clip_position.repaint() def updateDevices(self): for action in self.deviceGroup.actions(): self.deviceGroup.removeAction(action) self.menuDevice.removeAction(action) for device in self.devices: action = QAction(device.name, self.menuDevice) action.setCheckable(True) action.setData(device) self.menuDevice.addAction(action) self.deviceGroup.addAction(action) action.setChecked(True) self.device = device def addDevice(self, device): self.devices.append(device) self.updateDevices() self.is_learn_device_mode = False def onDeviceSelect(self): self.device = self.deviceGroup.checkedAction().data() if self.device: if self.device.init_command: for note in self.device.init_command: self.queue_out.put(note) self.redraw() def timebase_callback(self, state, nframes, pos, new_pos): if pos.frame_rate == 0: return None pos.valid = 0x10 pos.bar_start_tick = BAR_START_TICK pos.beats_per_bar = self.beat_per_bar.value() pos.beat_type = BEAT_TYPE pos.ticks_per_beat = TICKS_PER_BEAT pos.beats_per_minute = self.bpm.value() ticks_per_second = (pos.beats_per_minute * pos.ticks_per_beat) / 60 ticks = (ticks_per_second * pos.frame) / pos.frame_rate (beats, pos.tick) = divmod(int(round(ticks, 0)), int(round(pos.ticks_per_beat, 0))) (bar, beat) = divmod(beats, int(round(pos.beats_per_bar, 0))) (pos.bar, pos.beat) = (bar + 1, beat + 1) return None
class MainFileMenu(QMenu): """Menu with file actions.""" def __init__(self, parent, layer_editor, title='&File'): """Initializes the class.""" super(MainFileMenu, self).__init__(parent) self.setTitle(title) self._layer_editor = layer_editor self.parent = parent self._about_dialog = GMAboutDialog(parent, 'GeoMop LayerEditor') self._new_file_action = QAction('&New File ...', self) self._new_file_action.setShortcut( cfg.get_shortcut('new_file').key_sequence) self._new_file_action.setStatusTip('New layer data file') self._new_file_action.triggered.connect(self._layer_editor.new_file) self.addAction(self._new_file_action) self._open_file_action = QAction('&Open File ...', self) self._open_file_action.setShortcut( cfg.get_shortcut('open_file').key_sequence) self._open_file_action.setStatusTip('Open layer data file') self._open_file_action.triggered.connect(self._layer_editor.open_file) self.addAction(self._open_file_action) self._save_file_action = QAction('&Save File', self) self._save_file_action.setShortcut( cfg.get_shortcut('save_file').key_sequence) self._save_file_action.setStatusTip('Save layer data file') self._save_file_action.triggered.connect(self._layer_editor.save_file) self.addAction(self._save_file_action) self._save_as_action = QAction('Save &As ...', self) self._save_as_action.setShortcut( cfg.get_shortcut('save_file_as').key_sequence) self._save_as_action.setStatusTip('Save layer data file as') self._save_as_action.triggered.connect(self._layer_editor.save_as) self.addAction(self._save_as_action) self._recent_file_signal_connect = False self._recent = self.addMenu('Open &Recent Files') self._recent_group = QActionGroup(self, exclusive=True) self.addSeparator() self._import_file_action = QAction('&Add Shape File ...', self) self._import_file_action.setStatusTip('Add shape file') self._import_file_action.triggered.connect( self._layer_editor.add_shape_file) self.addAction(self._import_file_action) self.addSeparator() self._about_action = QAction('About', self) self._about_action.triggered.connect(self._on_about_action_clicked) self.addAction(self._about_action) self._help_dialog = LE_help_dialog(parent) self._help_action = QAction('Help', self) self._help_action.triggered.connect(self._on_help_action_clicked) self.addAction(self._help_action) self.addSeparator() self._exit_action = QAction('E&xit', self) self._exit_action.setShortcut(cfg.get_shortcut('exit').key_sequence) self._exit_action.setStatusTip('Exit application') self._exit_action.triggered.connect(self._exit_clicked) self.addAction(self._exit_action) def update_recent_files(self, from_row=1): """update recent file in menu""" if self._recent_file_signal_connect: self._recent_group.triggered.disconnect() self._recent_file_signal_connect = False for action in self._recent_group.actions(): self._recent_group.removeAction(action) if len(cfg.config.recent_files) < from_row + 1: self._recent.setEnabled(False) return self._recent.setEnabled(True) for i in range(from_row, len(cfg.config.recent_files)): action = QAction(cfg.config.recent_files[i], self, checkable=True) action.setData(cfg.config.recent_files[i]) reaction = self._recent_group.addAction(action) self._recent.addAction(reaction) self._recent_group.triggered.connect(self._layer_editor.open_recent) self._recent_file_signal_connect = True def _on_about_action_clicked(self): """Displays about dialog.""" if not self._about_dialog.isVisible(): self._about_dialog.show() def _on_help_action_clicked(self): """Displays help dialog.""" if not self._help_dialog.isVisible(): self._help_dialog.show() def _exit_clicked(self): """Performs actions before app is closed.""" # prompt user to save changes (if any) if not self._layer_editor.mainwindow.close(): return qApp.quit()
class Gui(QMainWindow, Ui_MainWindow): NOTEON = 0x9 NOTEOFF = 0x8 MIDICTRL = 11 GREEN = ("#cell_frame { border: 0px; border-radius: 10px; " "background-color: rgb(125,242,0);}") BLUE = ("#cell_frame { border: 0px; border-radius: 10px; " "background-color: rgb(0, 130, 240);}") RED = ("#cell_frame { border: 0px; border-radius: 10px; " "background-color: rgb(255, 21, 65);}") AMBER = ("#cell_frame { border: 0px; border-radius: 10px; " "background-color: rgb(255, 102, 0);}") PURPLE = ("#cell_frame { border: 0px; border-radius: 10px; " "background-color: rgb(130, 0, 240);}") DEFAULT = ("#cell_frame { border: 0px; border-radius: 10px; " "background-color: rgb(217, 217, 217);}") RECORD_BLINK = ("QPushButton {background-color: rgb(255, 255, 255);}" "QPushButton:pressed {background-color: " "rgb(98, 98, 98);}") RECORD_DEFAULT = ("QPushButton {background-color: rgb(0, 0, 0);}" "QPushButton:pressed {background-color: " "rgb(98, 98, 98);}") STATE_COLORS = {Clip.STOP: RED, Clip.STARTING: GREEN, Clip.START: GREEN, Clip.STOPPING: RED, Clip.PREPARE_RECORD: AMBER, Clip.RECORDING: AMBER} STATE_BLINK = {Clip.STOP: False, Clip.STARTING: True, Clip.START: False, Clip.STOPPING: True, Clip.PREPARE_RECORD: True, Clip.RECORDING: False} BLINK_DURATION = 200 PROGRESS_PERIOD = 300 updateUi = pyqtSignal() readQueueIn = pyqtSignal() def __init__(self, song, jack_client): QObject.__init__(self) super(Gui, self).__init__() self._jack_client = jack_client self.setupUi(self) self.clip_volume.knobRadius = 3 self.is_learn_device_mode = False self.queue_out, self.queue_in = Queue(), Queue() self.updateUi.connect(self.update) self.readQueueIn.connect(self.readQueue) self.current_vol_block = 0 # Load devices self.deviceGroup = QActionGroup(self.menuDevice) self.devices = [] settings = QSettings('superboucle', 'devices') if settings.contains('devices') and settings.value('devices'): for raw_device in settings.value('devices'): self.devices.append(Device(pickle.loads(raw_device))) else: self.devices.append(Device({'name': 'No Device', })) self.updateDevices() self.deviceGroup.triggered.connect(self.onDeviceSelect) # Load song self.initUI(song) self.actionNew.triggered.connect(self.onActionNew) self.actionOpen.triggered.connect(self.onActionOpen) self.actionSave.triggered.connect(self.onActionSave) self.actionSave_As.triggered.connect(self.onActionSaveAs) self.actionAdd_Device.triggered.connect(self.onAddDevice) self.actionManage_Devices.triggered.connect(self.onManageDevice) self.actionFullScreen.triggered.connect(self.onActionFullScreen) self.master_volume.valueChanged.connect(self.onMasterVolumeChange) self.rewindButton.clicked.connect(self.onRewindClicked) self.playButton.clicked.connect(self._jack_client.transport_start) self.pauseButton.clicked.connect(self._jack_client.transport_stop) self.gotoButton.clicked.connect(self.onGotoClicked) self.recordButton.clicked.connect(self.onRecord) self.clip_name.textChanged.connect(self.onClipNameChange) self.clip_volume.valueChanged.connect(self.onClipVolumeChange) self.beat_diviser.valueChanged.connect(self.onBeatDiviserChange) self.frame_offset.valueChanged.connect(self.onFrameOffsetChange) self.beat_offset.valueChanged.connect(self.onBeatOffsetChange) self.revertButton.clicked.connect(self.onRevertClip) self.normalizeButton.clicked.connect(self.onNormalizeClip) self.exportButton.clicked.connect(self.onExportClip) self.deleteButton.clicked.connect(self.onDeleteClipClicked) self.blktimer = QTimer() self.blktimer.state = False self.blktimer.timeout.connect(self.toogleBlinkButton) self.blktimer.start(self.BLINK_DURATION) self.disptimer = QTimer() self.disptimer.start(self.PROGRESS_PERIOD) self.disptimer.timeout.connect(self.updateProgress) self._jack_client.set_timebase_callback(self.timebase_callback) self.show() def initUI(self, song): # remove old buttons self.btn_matrix = [[None for y in range(song.height)] for x in range(song.width)] self.state_matrix = [[-1 for y in range(song.height)] for x in range(song.width)] for i in reversed(range(self.gridLayout.count())): self.gridLayout.itemAt(i).widget().close() self.gridLayout.itemAt(i).widget().setParent(None) self.song = song self.frame_clip.setEnabled(False) self.master_volume.setValue(song.volume*256) self.bpm.setValue(song.bpm) self.beat_per_bar.setValue(song.beat_per_bar) for x in range(song.width): for y in range(song.height): clip = song.clips_matrix[x][y] cell = Cell(self, clip, x, y) self.btn_matrix[x][y] = cell self.gridLayout.addWidget(cell, y, x) # send init command for init_cmd in self.device.init_command: self.queue_out.put(init_cmd) self.update() def closeEvent(self, event): settings = QSettings('superboucle', 'devices') settings.setValue('devices', [pickle.dumps(x.mapping) for x in self.devices]) def onStartStopClicked(self): clip = self.sender().parent().parent().clip self.startStop(clip.x, clip.y) def startStop(self, x, y): clip = self.btn_matrix[x][y].clip if clip is None: return if self.song.is_record: self.song.is_record = False self.updateRecordBtn() # calculate buffer size state, position = self._jack_client.transport_query() bps = position['beats_per_minute'] / 60 fps = position['frame_rate'] size = (1 / bps) * clip.beat_diviser * fps self.song.init_record_buffer(clip, 2, size, fps) # set frame offset based on jack block size clip.frame_offset = self._jack_client.blocksize clip.state = Clip.PREPARE_RECORD self.recordButton.setStyleSheet(self.RECORD_DEFAULT) else: self.song.toogle(clip.x, clip.y) self.update() def onEdit(self): self.last_clip = self.sender().parent().parent().clip if self.last_clip: self.frame_clip.setEnabled(True) self.clip_name.setText(self.last_clip.name) self.frame_offset.setValue(self.last_clip.frame_offset) self.beat_offset.setValue(self.last_clip.beat_offset) self.beat_diviser.setValue(self.last_clip.beat_diviser) self.clip_volume.setValue(self.last_clip.volume*256) state, position = self._jack_client.transport_query() fps = position['frame_rate'] bps = self.bpm.value() / 60 if self.bpm.value() and fps: size_in_beat = (bps/fps)*self.song.length(self.last_clip) else: size_in_beat = "No BPM info" clip_description = ("Size in sample : %s\nSize in beat : %s" % (self.song.length(self.last_clip), round(size_in_beat, 1))) self.clip_description.setText(clip_description) def onAddClipClicked(self): AddClipDialog(self, self.sender().parent().parent()) def onRevertClip(self): if self.last_clip and self.last_clip.audio_file: audio_file = self.last_clip.audio_file self.song.data[audio_file] = self.song.data[audio_file][::-1] def onNormalizeClip(self): if self.last_clip and self.last_clip.audio_file: audio_file = self.last_clip.audio_file absolute_val = np.absolute(self.song.data[audio_file]) current_level = np.ndarray.max(absolute_val) self.song.data[audio_file][:] *= (1 / current_level) def onExportClip(self): if self.last_clip and self.last_clip.audio_file: audio_file = self.last_clip.audio_file file_name, a = ( QFileDialog.getSaveFileName(self, 'Export Clip : %s' % self.last_clip.name, expanduser('~'), 'WAVE (*.wav)')) if file_name: file_name = verify_ext(file_name, 'wav') sf.write(self.song.data[audio_file], file_name, self.song.samplerate[audio_file], subtype=sf.default_subtype('WAV'), format='WAV') def onDeleteClipClicked(self): if self.last_clip: response = QMessageBox.question(self, "Delete Clip ?", ("Are you sure " "to delete the clip ?")) if response == QMessageBox.Yes: self.frame_clip.setEnabled(False) self.song.removeClip(self.last_clip) self.initUI(self.song) def onMasterVolumeChange(self): self.song.volume = (self.master_volume.value() / 256) def onStartClicked(self): pass self._jack_client.transport_start def onGotoClicked(self): state, position = self._jack_client.transport_query() new_position = (position['beats_per_bar'] * (self.gotoTarget.value() - 1) * position['frame_rate'] * (60 / position['beats_per_minute'])) self._jack_client.transport_locate(int(round(new_position, 0))) def onRecord(self): self.song.is_record = not self.song.is_record self.updateRecordBtn() def updateRecordBtn(self): if not self.song.is_record: self.recordButton.setStyleSheet(self.RECORD_DEFAULT) if self.device.record_btn: (msg_type, channel, pitch, velocity) = self.device.record_btn if self.song.is_record: color = self.device.blink_amber_vel else: color = self.device.black_vel self.queue_out.put(((msg_type << 4) + channel, pitch, color)) def onRewindClicked(self): self._jack_client.transport_locate(0) def onClipNameChange(self): self.last_clip.name = self.clip_name.text() tframe = self.btn_matrix[self.last_clip.x][self.last_clip.y] tframe.clip_name.setText(self.last_clip.name) def onClipVolumeChange(self): self.last_clip.volume = (self.clip_volume.value() / 256) def onBeatDiviserChange(self): self.last_clip.beat_diviser = self.beat_diviser.value() def onFrameOffsetChange(self): self.last_clip.frame_offset = self.frame_offset.value() def onBeatOffsetChange(self): self.last_clip.beat_offset = self.beat_offset.value() def onActionNew(self): NewSongDialog(self) def onActionOpen(self): file_name, a = ( QFileDialog.getOpenFileName(self, 'Open file', expanduser('~'), 'Super Boucle Song (*.sbs)')) if file_name: self.setEnabled(False) message = QMessageBox(self) message.setWindowTitle("Loading ....") message.setText("Reading Files, please wait ...") message.show() self.initUI(load_song_from_file(file_name)) message.close() self.setEnabled(True) def onActionSave(self): if self.song.file_name: self.song.save() else: self.onActionSaveAs() def onActionSaveAs(self): file_name, a = ( QFileDialog.getSaveFileName(self, 'Save As', expanduser('~'), 'Super Boucle Song (*.sbs)')) if file_name: file_name = verify_ext(file_name, 'sbs') self.song.file_name = file_name self.song.save() print("File saved to : {}".format(self.song.file_name)) def onAddDevice(self): self.learn_device = LearnDialog(self, self.addDevice) self.is_learn_device_mode = True def onManageDevice(self): ManageDialog(self) def onActionFullScreen(self): if self.isFullScreen(): self.showNormal() else: self.showFullScreen() self.show() def update(self): for x in range(len(self.song.clips_matrix)): line = self.song.clips_matrix[x] for y in range(len(line)): clp = line[y] if clp is None: state = None else: state = clp.state if state != self.state_matrix[x][y]: if clp: self.setCellColor(x, y, self.STATE_COLORS[state], self.STATE_BLINK[state]) try: self.queue_out.put(self.device.generateNote(x, y, state)) except IndexError: # print("No cell associated to %s x %s" # % (clp.x, clp.y)) pass self.state_matrix[x][y] = state def redraw(self): self.state_matrix = [[-1 for x in range(self.song.height)] for x in range(self.song.width)] self.update() def readQueue(self): try: while True: note = self.queue_in.get(block=False) if len(note) == 3: status, pitch, vel = struct.unpack('3B', note) channel = status & 0xF msg_type = status >> 4 self.processNote(msg_type, channel, pitch, vel) # else: # print("Invalid message length") except Empty: pass def processNote(self, msg_type, channel, pitch, vel): btn_id = (msg_type, channel, pitch, vel) btn_id_vel = (msg_type, channel, pitch, -1) ctrl_key = (msg_type, channel, pitch) # master volume if ctrl_key == self.device.master_volume_ctrl: self.song.master_volume = vel / 127 (self.master_volume .setValue(self.song.master_volume * 256)) elif self.device.play_btn in [btn_id, btn_id_vel]: self._jack_client.transport_start() elif self.device.pause_btn in [btn_id, btn_id_vel]: self._jack_client.transport_stop() elif self.device.rewind_btn in [btn_id, btn_id_vel]: self.onRewindClicked() elif self.device.goto_btn in [btn_id, btn_id_vel]: self.onGotoClicked() elif self.device.record_btn in [btn_id, btn_id_vel]: self.onRecord() elif ctrl_key in self.device.ctrls: try: ctrl_index = self.device.ctrls.index(ctrl_key) clip = (self.song.clips_matrix [ctrl_index] [self.current_vol_block]) if clip: clip.volume = vel / 127 if self.last_clip == clip: self.clip_volume.setValue(self.last_clip.volume * 256) except KeyError: pass elif (btn_id in self.device.block_buttons or btn_id_vel in self.device.block_buttons): try: self.current_vol_block = ( self.device.block_buttons.index(btn_id)) except ValueError: self.current_vol_block = ( self.device.block_buttons.index(btn_id_vel)) for i in range(len(self.device.block_buttons)): (a, b_channel, b_pitch, b) = self.device.block_buttons[i] if i == self.current_vol_block: color = self.device.red_vel else: color = self.device.black_vel self.queue_out.put(((self.NOTEON << 4) + b_channel, b_pitch, color)) else: x, y = -1, -1 try: x, y = self.device.getXY(btn_id) except IndexError: pass except KeyError: try: x, y = self.device.getXY(btn_id_vel) except KeyError: pass if (x >= 0 and y >= 0): self.startStop(x, y) def setCellColor(self, x, y, color, blink=False): self.btn_matrix[x][y].setStyleSheet(color) self.btn_matrix[x][y].blink = blink self.btn_matrix[x][y].color = color def toogleBlinkButton(self): for line in self.btn_matrix: for btn in line: if btn.blink: if self.blktimer.state: btn.setStyleSheet(btn.color) else: btn.setStyleSheet(self.DEFAULT) if self.song.is_record: if self.blktimer.state: self.recordButton.setStyleSheet(self.RECORD_BLINK) else: self.recordButton.setStyleSheet(self.RECORD_DEFAULT) self.blktimer.state = not self.blktimer.state def updateProgress(self): state, pos = self._jack_client.transport_query() if 'bar' in pos: bbt = "%d|%d|%03d" % (pos['bar'], pos['beat'], pos['tick']) else: bbt = "-|-|-" seconds = int(pos['frame'] / pos['frame_rate']) (minutes, second) = divmod(seconds, 60) (hour, minute) = divmod(minutes, 60) time = "%d:%02d:%02d" % (hour, minute, second) self.bbtLabel.setText("%s\n%s" % (bbt, time)) for line in self.btn_matrix: for btn in line: if btn.clip and btn.clip.audio_file: value = ((btn.clip.last_offset / self.song.length(btn.clip)) * 97) btn.clip_position.setValue(value) btn.clip_position.repaint() def updateDevices(self): for action in self.deviceGroup.actions(): self.deviceGroup.removeAction(action) self.menuDevice.removeAction(action) for device in self.devices: action = QAction(device.name, self.menuDevice) action.setCheckable(True) action.setData(device) self.menuDevice.addAction(action) self.deviceGroup.addAction(action) action.setChecked(True) self.device = device def addDevice(self, device): self.devices.append(device) self.updateDevices() self.is_learn_device_mode = False def onDeviceSelect(self): self.device = self.deviceGroup.checkedAction().data() if self.device: if self.device.init_command: for note in self.device.init_command: self.queue_out.put(note) self.redraw() def timebase_callback(self, state, nframes, pos, new_pos): pos.valid = 0x10 pos.bar_start_tick = BAR_START_TICK pos.beats_per_bar = self.beat_per_bar.value() pos.beat_type = BEAT_TYPE pos.ticks_per_beat = TICKS_PER_BEAT pos.beats_per_minute = self.bpm.value() ticks = frame2bbt(pos.frame, pos.ticks_per_beat, pos.beats_per_minute, pos.frame_rate) (beats, pos.tick) = divmod(int(round(ticks, 0)), int(round(pos.ticks_per_beat, 0))) (bar, beat) = divmod(beats, int(round(pos.beats_per_bar, 0))) (pos.bar, pos.beat) = (bar + 1, beat + 1) return None