示例#1
0
	def show_items(self,filedict,number):
		layout=QVBoxLayout()
		self.listView[number] = QtWidgets.QListView()
		self.listView[number].setObjectName("listView"+str(number))
		slm=QStringListModel()
		slm.title=number
		name=list()
		# print(filedict)
		for i in filedict:
			name.append(i)
		slm.qList=name
		slm.setStringList(slm.qList)
		slm.data=filedict
		self.listView[number].setModel(slm)
		self.listView[number].clicked.connect(self.show_detail)
		layout.addWidget(self.listView[number])
		self.tablist[number].setLayout(layout)
class PostgresProjectListModel(QObject):
    """Qt list model representing available projects saved in PostgreSQL."""

    model_changed = pyqtSignal()

    def __init__(self, host, port, dbname, schema, authcfg, sslmode, *args,
                 **kwargs):
        super().__init__(*args, **kwargs)
        self.host = host
        self.port = port
        self.dbname = dbname
        self.schema = schema
        self.authcfg = authcfg
        self.sslmode = sslmode
        self.model = QStringListModel()

    def refresh_data(self):
        """Refresh the list of projects saved in PostgreSQL."""
        fetch_projects = PostgresProjectDownloader(
            host=self.host,
            port=self.port,
            dbname=self.dbname,
            schema=self.schema,
            authcfg=self.authcfg,
            sslmode=self.sslmode,
        )
        self.model.setStringList(list(fetch_projects()))
        self.model_changed.emit()

    def project_at_index(self, index):
        """Return the name of project at given index."""
        return self.model.data(index, Qt.DisplayRole)

    def index_for_project_name(self, project_name):
        """Return the index of the given name in project list."""
        indices = self.model.match(self.model.index(0, 0), Qt.DisplayRole,
                                   project_name)
        return indices[0] if indices else None
示例#3
0
class SleaZynth(QMainWindow):

    autoSaveFile = 'auto.save'

    texsize = 512
    samplerate = 44100

    def __init__(self):
        QMainWindow.__init__(self)
        self.ui = Ui_MainWindow()
        self.ui.setupUi(self)
        self.show()

        self.initModelView()
        self.initSignals()

        self.initState()
        self.autoLoad()

        self.initAMaySyn()
        self.initAudio()

    def keyPressEvent(self, event):
        if event.key() == Qt.Key_Escape:
            self.close()

        elif event.key() == Qt.Key_F1:
            self.debugOutput()

        if event.modifiers() & Qt.ControlModifier:

            if event.key() == Qt.Key_S:
                self.autoSave()

            elif event.key() == Qt.Key_L:
                self.autoLoad()

            elif event.key() == Qt.Key_T:
                self.renderWhateverWasLast()

    def closeEvent(self, event):
        QApplication.quit()

    def initSignals(self):
        self.ui.btnChooseFilename.clicked.connect(self.loadAndImportMayson)
        self.ui.btnImport.clicked.connect(self.importMayson)
        self.ui.btnExport.clicked.connect(self.exportChangedMayson)
        self.ui.checkAutoReimport.clicked.connect(self.toggleAutoReimport)
        self.ui.checkAutoRender.clicked.connect(self.toggleAutoRender)

        self.ui.editFilename.editingFinished.connect(
            partial(self.updateStateFromUI, only='maysonFile'))
        self.ui.editBPM.editingFinished.connect(
            partial(self.updateStateFromUI, only='BPM'))
        self.ui.spinBOffset.valueChanged.connect(
            partial(self.updateStateFromUI, only='B_offset'))
        self.ui.spinBStop.valueChanged.connect(
            partial(self.updateStateFromUI, only='B_stop'))
        self.ui.spinLevelSyn.valueChanged.connect(
            partial(self.updateStateFromUI, only='level_syn'))
        self.ui.spinLevelDrum.valueChanged.connect(
            partial(self.updateStateFromUI, only='level_drum'))
        self.ui.checkWriteWAV.clicked.connect(
            partial(self.updateStateFromUI, only='writeWAV'))
        self.ui.spinTimeShift.valueChanged.connect(
            partial(self.updateStateFromUI, only='extraTimeShift'))

        self.ui.editTrackName.textChanged.connect(self.trackSetName)
        self.ui.spinTrackVolume.valueChanged.connect(self.trackSetVolume)
        self.ui.checkTrackMute.stateChanged.connect(self.trackSetMute)
        self.ui.spinModOn.valueChanged.connect(self.moduleSetModOn)
        self.ui.spinModTranspose.valueChanged.connect(self.moduleSetTranspose)
        self.ui.btnApplyPattern.clicked.connect(self.moduleSetPattern)
        self.ui.btnApplyNote.clicked.connect(self.noteApplyChanges)
        self.ui.btnTrackClone.clicked.connect(self.trackClone)
        self.ui.btnTrackDelete.clicked.connect(self.trackDelete)
        self.ui.btnRandomSynth.clicked.connect(self.trackSetRandomSynth)
        self.ui.btnRandomizeSynth.clicked.connect(self.synthRandomize)
        self.ui.btnSaveSynth.clicked.connect(self.synthHardClone)
        self.ui.btnApplySynthName.clicked.connect(self.synthChangeName)
        self.ui.btnReloadSyn.clicked.connect(self.loadSynthsFromSynFile)

        self.ui.btnRenderModule.clicked.connect(self.renderModule)
        self.ui.btnRenderTrack.clicked.connect(self.renderTrack)
        self.ui.btnRenderSong.clicked.connect(self.renderSong)
        self.ui.btnStopPlayback.clicked.connect(self.stopPlayback)

        # model/view signals
        self.ui.trackList.selectionModel().currentChanged.connect(
            self.trackLoad)
        self.ui.patternCombox.currentIndexChanged.connect(self.patternLoad)
        self.ui.moduleList.selectionModel().currentChanged.connect(
            self.moduleLoad)
        self.ui.synthList.selectionModel().currentChanged.connect(
            self.trackSetSynth)
        self.ui.noteList.selectionModel().currentChanged.connect(self.noteLoad)
        self.ui.drumList.selectionModel().currentChanged.connect(
            self.noteSetDrum)

    def initModelView(self):
        self.trackModel = TrackModel()
        self.ui.trackList.setModel(self.trackModel)
        self.moduleModel = ModuleModel()
        self.ui.moduleList.setModel(self.moduleModel)
        self.patternModel = PatternModel()
        self.ui.patternCombox.setModel(self.patternModel)
        self.noteModel = NoteModel()
        self.ui.noteList.setModel(self.noteModel)
        self.synthModel = QStringListModel()
        self.ui.synthList.setModel(self.synthModel)
        self.drumModel = QStringListModel()
        self.ui.drumList.setModel(self.drumModel)

        self.noteModel.dataChanged.connect(
            self.updateModulesWithChangedPattern)
        self.noteModel.reloadNoteParameters.connect(self.noteLoad)

    def initState(self):
        self.state = {
            'maysonFile': '',
            'autoReimport': False,
            'autoRender': False,
            'lastRendered': '',
            'writeWAV': False,
            'selectedTrack': 0,
            'selectedModule': 0,
            'extraTimeShift': 0,
        }
        self.info = {}
        self.patterns = []
        self.synths = []
        self.drumkit = []
        self.amaysyn = None
        self.fileObserver = None

    def loadAndImportMayson(self):
        name, _ = QFileDialog.getOpenFileName(
            self, 'Load MAYSON file', '', 'aMaySyn export *.mayson(*.mayson)')
        if name == '':
            return
        self.state['maysonFile'] = name
        self.state['title'], self.state[
            'synFile'] = self.getTitleAndSynFromMayson(name)
        self.autoSave()
        self.importMayson()

    def importMayson(self):
        maysonData = {}
        try:
            file = open(self.state['maysonFile'], 'r')
            maysonData = json.load(file)
        except FileNotFoundError:
            print(
                f"{self.state['maysonFile']} could not be imported. make sure that it exists, or choose another one."
            )
            self.loadAndImportMayson()
        except json.decoder.JSONDecodeError:
            print(
                f"{self.state['maysonFile']} is changing right now, pause for 1 sec..."
            )
            sleep(1)
            self.importMayson()
        finally:
            file.close()

        if maysonData == {}:
            return

        self.info = maysonData['info']
        self.info.update({'title': self.state['title']})
        if self.amaysyn is not None:
            self.amaysyn.updateState(info=self.info)

        self.trackModel.setTracks(maysonData['tracks'])
        self.patternModel.setPatterns(maysonData['patterns'])
        self.synthModel.setStringList(maysonData['synths'])
        self.drumModel.setStringList(maysonData['drumkit'])

        self.trackModel.layoutChanged.emit()
        if self.state['selectedTrack'] >= self.trackModel.rowCount():
            self.state['selectedTrack'] = 0
        self.selectIndex(self.ui.trackList, self.trackModel,
                         self.state['selectedTrack'])

        if self.state['selectedModule'] >= self.moduleModel.rowCount():
            self.state['selectedModule'] = 0
        self.selectIndex(self.ui.moduleList, self.moduleModel,
                         self.state['selectedModule'])

        self.noteModel.layoutChanged.emit()
        if self.noteModel.rowCount() > 0:
            self.selectIndex(self.ui.noteList, self.noteModel, 0)

        self.synthModel.layoutChanged.emit()

        self.drumModel.layoutChanged.emit()
        if self.drumIndex().isValid():
            self.selectIndex(self.ui.drumList, self.drumModel,
                             self.drumIndex().row())

        self.applyStateToUI()

    def exportChangedMayson(self):
        name, _ = QFileDialog.getSaveFileName(
            self, 'Export with Changes', self.state['maysonFile'],
            'aMaySyn export *.mayson(*.mayson)')
        if name == '':
            return
        data = {
            'info': self.info,
            'tracks': self.trackModel.tracks,
            'patterns': self.patternModel.patterns,
            'synths': self.synthModel.stringList(),
            'drumkit': self.drumModel.stringList(),
        }
        file = open(name, 'w')
        json.dump(data, file)
        file.close()

    def updateStateFromUI(self, only=None):
        if only is None or only == 'maysonFile':
            self.state.update({'maysonFile': self.ui.editFilename.text()})
        title, synFile = self.getTitleAndSynFromMayson(
            self.state['maysonFile'])
        self.state.update({'synFile': synFile})
        self.state.update({'title': title})
        self.info['title'] = title
        if only is None or only == 'BPM':
            self.info['BPM'] = self.ui.editBPM.text()
        if only is None or only == 'B_offset':
            self.info['B_offset'] = self.ui.spinBOffset.value()
        if only is None or only == 'B_stop':
            self.info['B_stop'] = self.ui.spinBStop.value()
        if only is None or only == 'level_syn':
            self.info['level_syn'] == self.ui.spinLevelSyn.value()
        if only is None or only == 'level_drum':
            self.info['level_drum'] == self.ui.spinLevelDrum.value()
        if only is None or only == 'writeWAV':
            self.state['writeWAV'] = self.ui.checkWriteWAV.isChecked()
        if only is None or only == 'extraTimeShift':
            self.state['extraTimeShift'] = self.ui.spinTimeShift.value()

        if self.amaysyn is not None:
            self.amaysyn.updateState(info=self.info, synFile=synFile)

    def applyStateToUI(self):
        self.ui.editFilename.setText(self.state['maysonFile'])
        # TODO: think about - do I want self.state['override']['BPM'] etc.??
        self.ui.editBPM.setText(self.info['BPM'])
        self.ui.spinBOffset.setValue(self.info['B_offset'])
        self.ui.spinBStop.setValue(self.info['B_stop'])
        self.ui.spinLevelSyn.setValue(self.info['level_syn'])
        self.ui.spinLevelDrum.setValue(self.info['level_drum'])
        self.ui.checkAutoReimport.setChecked(self.state['autoReimport'])
        self.ui.checkAutoRender.setChecked(self.state['autoRender'])
        self.ui.checkWriteWAV.setChecked(self.state['writeWAV'])
        self.ui.spinTimeShift.setValue(self.state['extraTimeShift'])

    def autoSave(self):
        file = open(self.autoSaveFile, 'w')
        json.dump(self.state, file)
        file.close()

    def autoLoad(self):
        loadState = {}
        try:
            file = open(self.autoSaveFile, 'r')
            loadState = json.load(file)
            file.close()
        except FileNotFoundError:
            pass

        for key in loadState:
            self.state[key] = loadState[key]

        if 'autoReimport' in self.state:
            self.toggleAutoReimport(self.state['autoReimport'])
        if 'maysonFile' not in self.state or self.state['maysonFile'] == '':
            self.loadAndImportMayson()
        else:
            self.importMayson()

    def toggleAutoRender(self, checked):
        self.state['autoRender'] = checked
        self.autoSave()

    def toggleAutoReimport(self, checked):
        self.state['autoReimport'] = checked
        self.autoSave()

        if self.fileObserver is not None:
            self.fileObserver.stop()
            self.fileObserver.join()
            self.fileObserver = None

        if checked:
            file = self.state['maysonFile']
            eventHandler = FileModifiedHandler(file)
            eventHandler.fileChanged.connect(
                self.importAndRender if self.state['autoRender'] else self.
                importMayson)
            self.fileObserver = Observer()
            self.fileObserver.schedule(eventHandler,
                                       path=path.dirname(file),
                                       recursive=False)
            self.fileObserver.start()

    def importAndRender(self):
        self.importMayson()
        if self.amaysyn is None:
            print(
                "You want to Reimport&Render, but why is aMaySyn not initialized? do some rendering first!"
            )
            return
        self.renderWhateverWasLast()

#################################### GENERAL HELPERS ###########################################

    def selectIndex(self, list, model, index):
        list.selectionModel().setCurrentIndex(
            model.createIndex(index, 0), QItemSelectionModel.SelectCurrent)

    def patternIndexOfName(self, name):
        patternNames = [p['name'] for p in self.patternModel.patterns]
        if name in patternNames:
            return patternNames.index(name)
        else:
            return None

    def getTitleAndSynFromMayson(self, maysonFile):
        synFile = '.'.join(maysonFile.split('.')[:-1]) + '.syn'
        title = '.'.join(path.basename(maysonFile).split('.')[:-1])
        return title, synFile

    def placeholder(self):
        print("FUNCTION NOT IMPLEMENTED. Sorrriiiiiiieee! (not sorry.)")

#################################### TRACK FUNCTIONALITY #######################################

    def track(self):
        return self.trackModel.tracks[self.trackIndex().row(
        )] if self.trackModel.rowCount() > 0 else None

    def trackIndex(self):
        return self.ui.trackList.currentIndex()

    def trackModelChanged(self):
        self.trackModel.dataChanged.emit(self.trackIndex(), self.trackIndex())

    def trackLoad(self, currentIndex):
        cTrack = self.trackModel.tracks[currentIndex.row()]
        self.ui.editTrackName.setText(cTrack['name'])
        self.ui.spinTrackVolume.setValue(100 * cTrack['par_norm'])
        self.ui.checkTrackMute.setChecked(not cTrack['mute'])
        self.moduleModel.setModules(cTrack['modules'])
        if len(cTrack['modules']) > 0:
            self.selectIndex(self.ui.moduleList, self.moduleModel,
                             cTrack['current_module'])
            self.moduleLoad()
        self.selectIndex(self.ui.synthList, self.synthModel,
                         cTrack['current_synth'])
        self.state['selectedTrack'] = currentIndex.row()

    def trackClone(self):
        self.trackModel.cloneRow(self.trackIndex().row())

    def trackDelete(self):
        self.trackModel.removeRow(self.trackIndex().row())

    def trackSetName(self, name):
        self.track()['name'] = name
        self.trackModelChanged()

    def trackSetVolume(self, value):
        self.track()['par_norm'] = round(value * .01, 3)
        self.trackModelChanged()

    def trackSetMute(self, state):
        self.track()['mute'] = (state != Qt.Checked)
        self.trackModelChanged()

    def trackSetSynth(self, index):
        self.track()['current_synth'] = self.synthModel.stringList().index(
            self.synthModel.data(index, Qt.DisplayRole))
        self.ui.editSynthName.setText(self.synthName())
        self.trackModelChanged()

        if self.synth()[0] == 'D':
            self.noteModel.useDrumkit(self.drumModel.stringList())
        else:
            self.noteModel.useDrumkit(None)

    def trackSetRandomSynth(self):
        randomIndex = self.synthModel.createIndex(
            randint(0,
                    len(self.instrumentSynths()) - 1), 0)
        self.ui.synthList.setCurrentIndex(randomIndex)

#################################### MODULE FUNCTIONALITY ######################################

    def module(self):
        return self.moduleModel.modules[self.moduleIndex().row(
        )] if self.moduleModel.rowCount() > 0 else None

    def moduleIndex(self):
        return self.ui.moduleList.currentIndex()

    def moduleModelChanged(self):
        self.moduleModel.dataChanged.emit(self.moduleIndex(),
                                          self.moduleIndex())

    def moduleLoad(self, currentIndex=None):
        if currentIndex is None:
            cModule = self.module()
        else:
            cModule = self.moduleModel.modules[currentIndex.row()]
            self.state['selectedModule'] = currentIndex.row()
        self.ui.patternCombox.setCurrentIndex(
            self.patternIndexOfName(cModule['pattern']['name']))
        self.ui.spinModOn.setValue(cModule['mod_on'])
        self.ui.spinModTranspose.setValue(cModule['transpose'])

    def moduleAssignPattern(self, pattern):
        self.module()['pattern'] = pattern  # deepcopy(pattern)

    def moduleSetPattern(self):
        self.moduleAssignPattern(self.pattern())
        self.moduleModelChanged()

    def moduleSetModOn(self, value):
        self.module()['mod_on'] = self.ui.spinModOn.value()
        self.moduleModelChanged()

    def moduleSetTranspose(self, value):
        self.module()['transpose'] = self.ui.spinModTranspose.value()
        self.moduleModelChanged()

#################################### PATTERN FUNCTIONALITY #####################################

    def pattern(self):
        return self.patternModel.patterns[
            self.patternIndex()] if self.patternModel.rowCount() > 0 else None

    def patternIndex(self):
        return self.ui.patternCombox.currentIndex()

    def patternLoad(self, currentIndex):
        cPattern = self.patternModel.patterns[currentIndex]
        self.noteModel.setNotes(cPattern['notes'])

    def updateModulesWithChangedPattern(self, rowBegin, rowEnd):
        self.moduleAssignPattern(self.pattern())
        self.moduleModelChanged()
        self.trackModel.updateModulesWithChangedPattern(self.pattern())
        self.trackModelChanged()

#################################### NOTE FUNCTIONALITY ########################################

    def note(self):
        return self.noteModel.notes[
            self.noteIndex().row()] if self.noteModel.rowCount() > 0 else None

    def noteIndex(self):
        return self.ui.noteList.currentIndex()

    def noteModelChanged(self):
        self.noteModel.dataChanged.emit(self.noteIndex(), self.noteIndex())

    def noteLoad(self, currentIndex):
        self.ui.editNote.setText(
            self.noteModel.data(currentIndex, Qt.DisplayRole))
        self.ui.editNote.setCursorPosition(0)

    def noteApplyChanges(self):
        self.noteModel.changeByString(self.noteIndex(),
                                      self.ui.editNote.text())

    def noteSetDrum(self, currentIndex):
        if self.drum() is not None:
            self.noteModel.changeDrumTo(self.noteIndex(), self.drum())

################################ SYNTH / DRUM FUNCTIONALITY ####################################

    def synth(self):
        return self.synthModel.data(self.ui.synthList.currentIndex(),
                                    Qt.DisplayRole)

    def synthName(self):
        return self.synth()[2:]

    def instrumentSynths(self):
        return [
            I_synth for I_synth in self.synthModel.stringList()
            if I_synth[0] == 'I'
        ]

    def drum(self):
        if not self.drumIndex():
            print("LOLOLOL DRUM INDEX IS NONE (should never happen)")
        if not self.drumIndex().isValid():
            print("LOLOLOL DRUM INDEX NOT VALID")
        return self.drumModel.data(self.drumIndex(), Qt.DisplayRole)

    def drumIndex(self):
        return self.ui.drumList.currentIndex()

    def synthRandomize(self):
        self.amaysyn.aMaySynatize(reshuffle_randoms=True)

    def synthHardClone(self):
        if self.synth()[0] == 'D':
            self.synthHardCloneDrum(self)
            return
        else:
            count = 0
            oldID = self.synthName()
            synths = self.instrumentSynths()
            while True:
                formID = oldID + '.' + str(count)
                print("TRYING", formID, synths)
                if 'I_' + formID not in synths: break
                count += 1

            try:
                formTemplate = next(
                    form for form in self.amaysyn.last_synatized_forms
                    if form['id'] == oldID)
                formType = formTemplate['type']
                formMode = formTemplate['mode']
                formBody = ' '.join(key + '=' + formTemplate[key]
                                    for key in formTemplate
                                    if key not in ['type', 'id', 'mode'])
                if formMode: formBody += ' mode=' + ','.join(formMode)
            except StopIteration:
                print(
                    "Current synth is not compiled yet. Do so and try again.")
                return
            except:
                print("could not CLONE HARD:", formID, formTemplate)
                raise
            else:
                with open(self.state['synFile'], mode='a') as filehandle:
                    filehandle.write('\n' + formType + 4 * ' ' + formID +
                                     4 * ' ' + formBody)
                self.loadSynthsFromSynFile()

    def synthHardDrum(self):
        print("NOT IMPLEMENTED YET")
        return
        count = 0
        oldID = self.synthName()
        synths = self.instrumentSynths()
        while True:
            formID = oldID + '.' + str(count)
            print("TRYING", formID, synths)
            if 'I_' + formID not in synths: break
            count += 1

        try:
            formTemplate = next(form
                                for form in self.amaysyn.last_synatized_forms
                                if form['id'] == oldID)
            formType = formTemplate['type']
            formMode = formTemplate['mode']
            formBody = ' '.join(key + '=' + formTemplate[key]
                                for key in formTemplate
                                if key not in ['type', 'id', 'mode'])
            if formMode: formBody += ' mode=' + ','.join(formMode)
        except StopIteration:
            print("Current synth is not compiled yet. Do so and try again.")
            return
        except:
            print("could not CLONE HARD:", formID, formTemplate)
            raise
        else:
            with open(self.state['synFile'], mode='a') as filehandle:
                filehandle.write('\n' + formType + 4 * ' ' + formID + 4 * ' ' +
                                 formBody)
            self.loadSynthsFromSynFile()

    def synthChangeName(self):
        if self.synth()[0] != 'I':
            print("Nah. Select an instrument synth (I_blabloo)")
            return
        newID = self.ui.editSynthName.text()
        if newID == '':
            return
        formID = self.synthName()
        tmpFile = self.state['synFile'] + '.tmp'
        move(self.state['synFile'], tmpFile)
        with open(tmpFile, mode='r') as tmp_handle:
            with open(self.state['synFile'], mode='w') as new_handle:
                for line in tmp_handle.readlines():
                    lineparse = line.split()
                    if len(lineparse) > 2 and lineparse[0] in [
                            'main', 'maindrum'
                    ] and lineparse[1] == formID:
                        new_handle.write(
                            line.replace(' ' + formID + ' ',
                                         ' ' + newID + ' '))
                    else:
                        new_handle.write(line)
        self.loadSynthsFromSynFile()

    def loadSynthsFromSynFile(self):
        self.amaysyn.aMaySynatize()
        self.synthModel.setStringList(self.amaysyn.synths)
        self.synthModel.dataChanged.emit(
            self.synthModel.createIndex(0, 0),
            self.synthModel.createIndex(self.synthModel.rowCount(), 0))
        self.drumModel.setStringList(self.amaysyn.drumkit)
        self.drumModel.dataChanged.emit(
            self.drumModel.createIndex(0, 0),
            self.drumModel.createIndex(self.drumModel.rowCount(), 0))
        self.trackModel.setSynthList(self.amaysyn.synths)
        self.trackModelChanged()

# TODO: function to change drumkit order / assignment?

######################################## SleaZYNTHesizer #######################################

    def initAMaySyn(self):
        self.amaysyn = aMaySynBuilder(self, self.state['synFile'], self.info)

    def initAudio(self):
        self.audioformat = QAudioFormat()
        self.audioformat.setSampleRate(self.samplerate)
        self.audioformat.setChannelCount(2)
        self.audioformat.setSampleSize(32)
        self.audioformat.setCodec('audio/pcm')
        self.audioformat.setByteOrder(QAudioFormat.LittleEndian)
        self.audioformat.setSampleType(QAudioFormat.Float)
        self.audiooutput = QAudioOutput(self.audioformat)
        self.audiooutput.setVolume(1.0)

    def stopPlayback(self):
        self.audiooutput.stop()

    def renderWhateverWasLast(self):
        if self.state['lastRendered'] == 'module':
            self.renderModule()
        elif self.state['lastRendered'] == 'track':
            self.renderTrack()
        else:
            self.renderSong()

    def renderModule(self):
        print(self.track(), self.module())
        self.state['lastRendered'] = 'module'
        restoreMute = self.track()['mute']
        self.track()['mute'] = False
        modInfo = deepcopy(self.info)
        modInfo['B_offset'] = self.module()['mod_on']
        modInfo['B_stop'] = self.module()['mod_on'] + self.module(
        )['pattern']['length']
        self.amaysyn.info = modInfo
        self.amaysyn.extra_time_shift = self.state['extraTimeShift']
        shader = self.amaysyn.build(tracks=[self.track()],
                                    patterns=[self.module()['pattern']])
        self.amaysyn.info = self.info
        self.track()['mute'] = restoreMute
        self.executeShader(shader)

    def renderTrack(self):
        self.state['lastRendered'] = 'track'
        restoreMute = self.track()['mute']
        self.track()['mute'] = False
        self.amaysyn.extra_time_shift = self.state['extraTimeShift']
        shader = self.amaysyn.build(tracks=[self.track()],
                                    patterns=self.patternModel.patterns)
        self.track()['mute'] = restoreMute
        self.executeShader(shader)

    def renderSong(self):
        self.state['lastRendered'] = 'song'
        self.amaysyn.extra_time_shift = self.state['extraTimeShift']
        shader = self.amaysyn.build(tracks=self.trackModel.tracks,
                                    patterns=self.patternModel.patterns)
        self.executeShader(shader)

    def executeShader(self, shader):
        self.ui.codeEditor.clear()
        self.ui.codeEditor.insertPlainText(
            shader.replace(4 * ' ', '\t').replace(3 * ' ', '\t'))
        self.ui.codeEditor.ensureCursorVisible()

        sequenceLength = len(
            self.amaysyn.sequence) if self.amaysyn.sequence is not None else 0
        if not self.amaysyn.useSequenceTexture and sequenceLength > pow(2, 14):
            QMessageBox.critical(
                self, "I CAN'T",
                f"Either switch to using the Sequence Texture (ask QM), or reduce the sequence size by limiting the offset/stop positions or muting tracks.\nCurrent sequence length is:\n{sequenceLength} > {pow(2,14)}"
            )
            return

        self.bytearray = self.amaysyn.executeShader(
            shader,
            self.samplerate,
            self.texsize,
            renderWAV=self.state['writeWAV'])
        self.audiobuffer = QBuffer(self.bytearray)
        self.audiobuffer.open(QIODevice.ReadOnly)
        self.audiooutput.stop()
        self.audiooutput.start(self.audiobuffer)

###################################### DEBUG STUFF #############################################

    def debugOutput(self):
        print("TRACKS:", self.trackModel.rowCount())
        print("===== TRACK ACCUMULATION =====")
        track_accumulate = 0
        for t in self.trackModel.tracks:
            delta = len(t['modules'])
            print(
                f"{t['name']:>20} {track_accumulate:>10} {track_accumulate + delta:>10}"
            )
            track_accumulate += delta
        print("END AT", track_accumulate)
        print()
        print("PATTERNS:", self.patternModel.rowCount())
        print("===== PATTERN ACCUMULATION =====")
        pattern_accumulate = 0
        for p in self.patternModel.patterns:
            delta = len(p['notes'])
            print(
                f"{p['name']:>20} {pattern_accumulate:>10} {pattern_accumulate + delta:>10}"
            )
            pattern_accumulate += delta
        print("END AT", pattern_accumulate)
        print()
示例#4
0
class MyControlPanel(QWidget):
    flagselected = pyqtSignal(int)

    def __init__(self, model, parent=None):
        QWidget.__init__(self, parent)
        self.ui = Ui_ControlPanel()
        self.ui.setupUi(self)
        self.model = model

        sortmenu = QMenu()
        lIinitial = None
        searchLegend = model.datafield(model.cfg.legendInitial)
        for i, col in enumerate(
                sorted(set(self.model.cols) - self.model.cfg.internalCols,
                       key=self.model.prettyname)):
            # Sort Menu
            a = sortmenu.addAction(self.model.prettyname(col))
            a.setData(col)
            a.triggered.connect(self.onAddSortField)

            # Combo boxes
            self.ui.partComboBox.addItem(self.model.prettyname(col), col)
            self.ui.legendComboBox.addItem(self.model.prettyname(col), col)
            if col == searchLegend:
                lIinitial = i

        # Sort
        self.ui.addSortField.setMenu(sortmenu)
        self.ui.sortByListWidget.itemSelectionChanged.connect(
            self.onSelectionChange)
        self.onSelectionChange()
        self.ui.removeSortField.clicked.connect(self.onRemoveSortField)
        self.ui.moveSortField.clicked.connect(self.onMoveSortFieldUp)
        self.ui.sortAscendingCheckBox.clicked.connect(
            self.onSortAscendingChange)

        # Legend
        self.legend = None
        self.ui.legendGroupBox.setChecked(lIinitial is not None)
        self.legendModel = QStandardItemModel()
        self.ui.legendListView.setModel(self.legendModel)
        self.ui.legendListView.doubleClicked.connect(self.selectLegendColor)

        if lIinitial is not None:
            self.ui.legendComboBox.setCurrentIndex(lIinitial)
        self.ui.legendComboBox.currentIndexChanged.connect(
            self.onLegendComboChange)
        self.onLegendComboChange()

        self.ui.legendGroupBox.toggled.connect(self.calcLegend)
        self.ui.legendBinSpinBox.editingFinished.connect(self.calcLegend)
        self.ui.legendMaxSpinBox.editingFinished.connect(self.calcLegend)
        self.ui.legendMinSpinBox.editingFinished.connect(self.calcLegend)
        self.legendModel.dataChanged.connect(self.updateModelLegend)

        # Partition
        self.partitions = None
        self.partitionModel = QStringListModel()
        self.ui.partitionList.setModel(self.partitionModel)
        self.ui.partitionList.selectionModel().currentChanged.connect(
            self.onSelectPartition)

        self.ui.partComboBox.currentIndexChanged.connect(
            self.onPartitionComboChange)
        self.onPartitionComboChange()

        self.ui.partitionBox.toggled.connect(self.calcPartitions)
        self.ui.partBinSpinBox.valueChanged.connect(self.calcPartitions)
        self.ui.partMaxSpinBox.valueChanged.connect(self.calcPartitions)
        self.ui.partMinSpinBox.valueChanged.connect(self.calcPartitions)

        # Flags
        if self.model.flagFilter.isActive():
            self.ui.flagIgnore.setChecked(True)
            if self.model.flagFilter.invert:
                self.ui.flagExclude.setChecked(True)
            else:
                self.ui.flagKeep.setChecked(True)
        else:
            self.ui.flagIgnore.setChecked(True)
        self.model.flagFilter.filterchange.connect(self.onFlagChange)
        self.flagModel = QStringListModel()
        self.ui.flaggedList.setModel(self.flagModel)
        self.ui.flagIgnore.clicked.connect(self.onIgnoreFlags)
        self.ui.flagExclude.clicked.connect(self.onExcludeFlags)
        self.ui.flagKeep.clicked.connect(self.onOnlyFlags)
        self.ui.flaggedList.selectionModel().currentChanged.connect(
            self.onSelectFlag)

        # Limits
        self.ui.limitCheckBox.clicked.connect(self.onLimitChange)
        self.ui.limTopButton.clicked.connect(self.onLimitChange)
        self.ui.limRandomButton.clicked.connect(self.onLimitChange)
        self.ui.limitSpinBox.editingFinished.connect(self.onLimitChange)
        self.onLimitChange()

        # Filters
        self.filtermodel = FilterModel(model.topfilter, model)
        self.ui.filterTreeView.setEditTriggers(
            QAbstractItemView.AllEditTriggers)
        self.ui.filterTreeView.setModel(self.filtermodel)
        if qt5:
            self.ui.filterTreeView.header().setSectionResizeMode(
                0, QHeaderView.ResizeToContents)
            self.ui.filterTreeView.header().setSectionResizeMode(
                1, QHeaderView.ResizeToContents)
        else:
            self.ui.filterTreeView.header().setResizeMode(
                0, QHeaderView.ResizeToContents)
            self.ui.filterTreeView.header().setResizeMode(
                1, QHeaderView.ResizeToContents)
        self.ui.filterTreeView.setItemDelegate(
            filterEditDelegate.FilterItemDelegate())
        self.filtermodel.rowsInserted.connect(self.ui.filterTreeView.expandAll)
        self.ui.filterTreeView.expandAll()

        # Filters Save/Load
        self.ui.saveFiltersButton.clicked.connect(self.onSaveFilters)
        self.ui.loadFiltersButton.clicked.connect(self.onLoadFilters)

    # Sorting
    def onAddSortField(self):
        field = self.sender().data()
        self.ui.sortByListWidget.addItem(self.model.prettyname(field))
        self.model.sortlst.append(field)
        self.model.onSortChange()

    def onSelectionChange(self):
        hasSelection = bool(len(self.ui.sortByListWidget.selectedItems()))
        self.ui.removeSortField.setEnabled(hasSelection)
        canMove = hasSelection
        for item in self.ui.sortByListWidget.selectedItems():
            canMove &= self.ui.sortByListWidget.row(item) != 0
        self.ui.moveSortField.setEnabled(canMove)

    def onRemoveSortField(self):
        rows = []
        for item in self.ui.sortByListWidget.selectedItems():
            rows.append(self.ui.sortByListWidget.row(item))
        for r in sorted(rows, reverse=True):
            del self.model.sortlst[r]
            self.ui.sortByListWidget.takeItem(r)
        self.model.onSortChange()
        self.onSelectionChange()

    def onMoveSortFieldUp(self):
        rows = []
        for item in self.ui.sortByListWidget.selectedItems():
            rows.append(self.ui.sortByListWidget.row(item))
        for r in sorted(rows):
            self.model.sortlst.insert(r - 1, self.model.sortlst.pop(r))
            self.ui.sortByListWidget.insertItem(
                r - 1, self.ui.sortByListWidget.takeItem(r))
            self.ui.sortByListWidget.setCurrentItem(
                self.ui.sortByListWidget.item(r - 1))
        self.model.onSortChange()

    def setSort(self, lst):
        for field in lst:
            self.ui.sortByListWidget.addItem(self.model.prettyname(field))
            self.model.sortlst.append(field)
        self.model.onSortChange()

    def onSortAscendingChange(self):
        self.model.reverseSort = not self.ui.sortAscendingCheckBox.isChecked()
        self.model.onSortChange()

    # Legends
    def onLegendComboChange(self):
        field = self.ui.legendComboBox.itemData(
            self.ui.legendComboBox.currentIndex())
        enable = not self.model.isCategorical(field)

        if self.ui.legendGroupBox.isChecked() or not enable:
            self.ui.legendBinSpinBox.setEnabled(enable)
            self.ui.legendMinSpinBox.setEnabled(enable)
            self.ui.legendMaxSpinBox.setEnabled(enable)

        self.ui.legendMinSpinBox.setMinimum(self.model.fieldmin(field))
        self.ui.legendMaxSpinBox.setMinimum(self.model.fieldmin(field))
        self.ui.legendMinSpinBox.setMaximum(self.model.fieldmax(field))
        self.ui.legendMaxSpinBox.setMaximum(self.model.fieldmax(field))

        self.ui.legendMinSpinBox.setValue(self.model.fieldmin(field))
        self.ui.legendMaxSpinBox.setValue(self.model.fieldmax(field))
        self.calcLegend()

    def calcLegend(self):
        self.legendModel.clear()
        if not self.ui.legendGroupBox.isChecked():
            self.legend = None
            self.legendModel.clear()
            self.updateModelLegend()
            return
        field = self.ui.legendComboBox.itemData(
            self.ui.legendComboBox.currentIndex())
        num = None
        minimum = None
        maximum = None
        if self.ui.legendBinSpinBox.isEnabled():
            num = self.ui.legendBinSpinBox.value()
            minimum = self.ui.legendMinSpinBox.value()
            maximum = self.ui.legendMaxSpinBox.value()
        self.legend = self.model.partition(field, minimum, maximum, num)
        lst = list(sorted(self.legend.keys()))
        for i, s in enumerate(lst):
            c = self.model.cfg.color(field, s, i)
            item = QStandardItem(s)
            item.setData(QColor(c), Qt.DecorationRole)
            item.setData(s)
            self.legendModel.appendRow(item)
        self.updateModelLegend()

    def selectLegendColor(self, index):
        c = QColorDialog.getColor(
            self.legendModel.data(index, Qt.DecorationRole), self)
        if c.isValid():
            self.legendModel.setData(index, c, Qt.DecorationRole)

    def updateModelLegend(self):
        stack = None
        if self.legend is not None:
            stack = [[], [], []]
            for i in range(self.legendModel.rowCount()):
                it = self.legendModel.item(i)
                stack[0].append(self.legend[it.data()])
                stack[1].append(it.data(Qt.DecorationRole).name())
                stack[2].append(it.text())
        self.model.setStacks(stack)

    # Partitions
    def onPartitionComboChange(self):
        field = self.ui.partComboBox.itemData(
            self.ui.partComboBox.currentIndex())
        enable = not self.model.isCategorical(field)

        if self.ui.partitionBox.isChecked() or not enable:
            self.ui.partBinSpinBox.setEnabled(enable)
            self.ui.partMinSpinBox.setEnabled(enable)
            self.ui.partMaxSpinBox.setEnabled(enable)

        self.ui.partMinSpinBox.setMinimum(self.model.fieldmin(field))
        self.ui.partMaxSpinBox.setMinimum(self.model.fieldmin(field))
        self.ui.partMinSpinBox.setMaximum(self.model.fieldmax(field))
        self.ui.partMaxSpinBox.setMaximum(self.model.fieldmax(field))

        self.ui.partMinSpinBox.setValue(self.model.fieldmin(field))
        self.ui.partMaxSpinBox.setValue(self.model.fieldmax(field))
        self.calcPartitions()

    def calcPartitions(self):
        self.model.clearPartition()
        if not self.ui.partitionBox.isChecked():
            self.partitions = None
            self.partitionModel.setStringList([])
            return
        field = self.ui.partComboBox.itemData(
            self.ui.partComboBox.currentIndex())
        num = None
        minimum = None
        maximum = None
        if self.ui.partBinSpinBox.isEnabled():
            num = self.ui.partBinSpinBox.value()
            minimum = self.ui.partMinSpinBox.value()
            maximum = self.ui.partMaxSpinBox.value()
        self.partitions = self.model.partition(field, minimum, maximum, num)
        self.partitionModel.setStringList(sorted(self.partitions.keys()))

    def onSelectPartition(self):
        part = self.partitionModel.data(
            self.ui.partitionList.selectionModel().currentIndex(),
            Qt.DisplayRole)
        self.model.setPartition(self.partitions[part])

    # Flags
    def onFlagChange(self):
        slist = []
        for i in np.where(self.model.flagFilter.keep)[0]:
            slist.append(str(i))
        self.flagModel.setStringList(slist)

    def onIgnoreFlags(self, checked):
        if checked:
            self.model.flagFilter.setActive(False)

    def onExcludeFlags(self, checked):
        if checked:
            self.model.flagFilter.setInvert(True)
            self.model.flagFilter.setActive(True)

    def onOnlyFlags(self, checked):
        if checked:
            self.model.flagFilter.setInvert(False)
            self.model.flagFilter.setActive(True)

    def onSelectFlag(self):
        row = int(
            self.flagModel.data(
                self.ui.flaggedList.selectionModel().currentIndex(),
                Qt.DisplayRole))
        self.flagselected.emit(row)

    # Limits
    def onLimitChange(self):
        if self.ui.limitCheckBox.isChecked():
            self.model.limit = self.ui.limitSpinBox.value()
        else:
            self.model.limit = None
        self.model.limitModeRandom = self.ui.limRandomButton.isChecked()

    def setLimit(self, l):
        self.ui.limitSpinBox.setValue(l)
        self.ui.limitCheckBox.setChecked(True)
        self.onLimitChange()

    # Filters
    def onSaveFilters(self):
        name = QFileDialog.getSaveFileName(self,
                                           'Save Filters As',
                                           filter='*.xml')
        if qt5:
            if name:
                self.model.saveFilters(name[0])
        elif name is not None and len(name):
            self.model.saveFilters(name)

    def onLoadFilters(self):
        name = QFileDialog.getOpenFileName(self,
                                           'Load Filter File',
                                           filter='*.xml')
        if qt5:
            if name:
                self.model.loadFilters(name[0])
        elif name is not None and len(name):
            self.model.loadFilters(name)
示例#5
0
class MyControlPanel(QWidget):
    flagselected = pyqtSignal(int)

    def __init__(self, model, parent=None):
        QWidget.__init__(self, parent)
        self.ui = Ui_ControlPanel()
        self.ui.setupUi(self)
        self.model = model

        sortmenu = QMenu()
        for i, col in enumerate(
                sorted(set(self.model.cols) - self.model.cfg.internalCols,
                       key=self.model.prettyname)):
            # Sort Menu
            a = sortmenu.addAction(self.model.prettyname(col))
            a.setData(col)
            a.triggered.connect(self.onAddSortField)

        # Sort
        self.ui.addSortField.setMenu(sortmenu)
        self.ui.sortByListWidget.itemSelectionChanged.connect(
            self.onSelectionChange)
        self.onSelectionChange()
        self.ui.removeSortField.clicked.connect(self.onRemoveSortField)
        self.ui.moveSortField.clicked.connect(self.onMoveSortFieldUp)
        self.ui.sortAscendingCheckBox.clicked.connect(
            self.onSortAscendingChange)

        # Legend
        self.legendWidget = partitionWidget.MyPartitionWidget(
            model, self.model.setStacks, model.cfg.legendInitial, True, True,
            self)
        self.legendWidget.ui.description.setText(
            "Change field and colors for stacked histograms. Double click to edit color. Single click on selected to edit label. Checkboxes only affect visibility, not the selected items."
        )
        self.legendWidget.ui.groupBox.setTitle("Histogram Legends")
        self.ui.verticalLayout_7.insertWidget(1, self.legendWidget)

        # Partition
        self.partWidget = partitionWidget.MyPartitionWidget(
            model, self.model.setPartition, model.cfg.partitionInitial, True,
            False, self)
        self.partWidget.ui.description.setText(
            "Partition the data. Selecting a partition changes the full dataset shown in plots to just match the partition. When partitions are active, output is split with one output for each partition."
        )
        self.partWidget.ui.groupBox.setTitle("Partitions")
        self.ui.verticalLayout_7.insertWidget(2, self.partWidget)

        # Flags
        if self.model.flagFilter.isActive():
            self.ui.flagIgnore.setChecked(True)
            if self.model.flagFilter.invert:
                self.ui.flagExclude.setChecked(True)
            else:
                self.ui.flagKeep.setChecked(True)
        else:
            self.ui.flagIgnore.setChecked(True)
        self.model.flagFilter.filterchange.connect(self.onFlagChange)
        self.flagModel = QStringListModel()
        self.ui.flaggedList.setModel(self.flagModel)
        self.ui.flagIgnore.clicked.connect(self.onIgnoreFlags)
        self.ui.flagExclude.clicked.connect(self.onExcludeFlags)
        self.ui.flagKeep.clicked.connect(self.onOnlyFlags)
        self.ui.flaggedList.selectionModel().currentChanged.connect(
            self.onSelectFlag)

        # Limits
        self.ui.limitCheckBox.clicked.connect(self.onLimitChange)
        self.ui.limTopButton.clicked.connect(self.onLimitChange)
        self.ui.limRandomButton.clicked.connect(self.onLimitChange)
        self.ui.limitSpinBox.editingFinished.connect(self.onLimitChange)
        self.onLimitChange()

        # Filters
        self.filtermodel = FilterModel(model.topfilter, model)
        self.ui.filterTreeView.setEditTriggers(
            QAbstractItemView.AllEditTriggers)
        self.ui.filterTreeView.setModel(self.filtermodel)
        if qt5:
            self.ui.filterTreeView.header().setSectionResizeMode(
                0, QHeaderView.ResizeToContents)
            self.ui.filterTreeView.header().setSectionResizeMode(
                1, QHeaderView.ResizeToContents)
        else:
            self.ui.filterTreeView.header().setResizeMode(
                0, QHeaderView.ResizeToContents)
            self.ui.filterTreeView.header().setResizeMode(
                1, QHeaderView.ResizeToContents)
        self.ui.filterTreeView.setItemDelegate(
            filterEditDelegate.FilterItemDelegate())
        self.filtermodel.rowsInserted.connect(self.ui.filterTreeView.expandAll)
        self.ui.filterTreeView.expandAll()

        # Filters Save/Load
        self.ui.saveFiltersButton.clicked.connect(self.onSaveFilters)
        self.ui.loadFiltersButton.clicked.connect(self.onLoadFilters)

    # Sorting
    def onAddSortField(self):
        field = self.sender().data()
        self.ui.sortByListWidget.addItem(self.model.prettyname(field))
        self.model.sortlst.append(field)
        self.model.onSortChange()

    def onSelectionChange(self):
        hasSelection = bool(len(self.ui.sortByListWidget.selectedItems()))
        self.ui.removeSortField.setEnabled(hasSelection)
        canMove = hasSelection
        for item in self.ui.sortByListWidget.selectedItems():
            canMove &= self.ui.sortByListWidget.row(item) != 0
        self.ui.moveSortField.setEnabled(canMove)

    def onRemoveSortField(self):
        rows = []
        for item in self.ui.sortByListWidget.selectedItems():
            rows.append(self.ui.sortByListWidget.row(item))
        for r in sorted(rows, reverse=True):
            del self.model.sortlst[r]
            self.ui.sortByListWidget.takeItem(r)
        self.model.onSortChange()
        self.onSelectionChange()

    def onMoveSortFieldUp(self):
        rows = []
        for item in self.ui.sortByListWidget.selectedItems():
            rows.append(self.ui.sortByListWidget.row(item))
        for r in sorted(rows):
            self.model.sortlst.insert(r - 1, self.model.sortlst.pop(r))
            self.ui.sortByListWidget.insertItem(
                r - 1, self.ui.sortByListWidget.takeItem(r))
            self.ui.sortByListWidget.setCurrentItem(
                self.ui.sortByListWidget.item(r - 1))
        self.model.onSortChange()

    def setSort(self, lst):
        for field in lst:
            self.ui.sortByListWidget.addItem(self.model.prettyname(field))
            self.model.sortlst.append(field)
        self.model.onSortChange()

    def onSortAscendingChange(self):
        self.model.reverseSort = not self.ui.sortAscendingCheckBox.isChecked()
        self.model.onSortChange()

    # Flags
    def onFlagChange(self):
        slist = []
        for i in np.where(self.model.flagFilter.keep)[0]:
            slist.append(str(i))
        self.flagModel.setStringList(slist)

    def onIgnoreFlags(self, checked):
        if checked:
            self.model.flagFilter.setActive(False)

    def onExcludeFlags(self, checked):
        if checked:
            self.model.flagFilter.setInvert(True)
            self.model.flagFilter.setActive(True)

    def onOnlyFlags(self, checked):
        if checked:
            self.model.flagFilter.setInvert(False)
            self.model.flagFilter.setActive(True)

    def onSelectFlag(self):
        row = int(
            self.flagModel.data(
                self.ui.flaggedList.selectionModel().currentIndex(),
                Qt.DisplayRole))
        self.flagselected.emit(row)

    # Limits
    def onLimitChange(self):
        if self.ui.limitCheckBox.isChecked():
            self.model.limit = self.ui.limitSpinBox.value()
        else:
            self.model.limit = None
        self.model.limitModeRandom = self.ui.limRandomButton.isChecked()

    def setLimit(self, l):
        self.ui.limitSpinBox.setValue(l)
        self.ui.limitCheckBox.setChecked(True)
        self.onLimitChange()

    # Filters
    def onSaveFilters(self):
        name = QFileDialog.getSaveFileName(self,
                                           'Save Filters As',
                                           filter='*.xml')
        if qt5:
            if name:
                self.model.saveFilters(name[0])
        elif name is not None and len(name):
            self.model.saveFilters(name)

    def onLoadFilters(self):
        name = QFileDialog.getOpenFileName(self,
                                           'Load Filter File',
                                           filter='*.xml')
        if qt5:
            if name:
                self.model.loadFilters(name[0])
        elif name is not None and len(name):
            self.model.loadFilters(name)
class MainApp(QWidget):
    STRANGER_DANGER = 350
    IMAGE_SIZE = (100, 100)

    stranger_color = (179, 20, 20)
    recognized_color = (59, 235, 62)

    def __init__(self, fps=30, parent=None):
        # type: (int, Optional[QWidget]) -> None
        super().__init__(parent=parent)

        self.pkg_path = path.dirname(path.dirname(path.abspath(__file__)))
        self.training_data_dir = path.join(self.pkg_path, 'train')
        self.models_dir = path.join(self.pkg_path, 'models')
        self.model_fname = 'fisherfaces.p'

        try:
            self.model = data_provider.load_model(
                path.join(self.models_dir, self.model_fname))
        except AssertionError:
            self.model = None

        self.existing_labels = QStringListModel(self.get_existing_labels())

        self.fps = fps
        self.video_size = QSize(640, 480)

        self.gray_image = None
        self.detected_faces = []

        # Setup the UI
        self.main_layout = QHBoxLayout()
        self.setLayout(self.main_layout)

        self.control_layout = QVBoxLayout()
        self.control_layout.setSpacing(8)
        self.main_layout.addItem(self.control_layout)

        # Setup the existing label view
        self.labels_view = QListView(parent=self)
        self.labels_view.setModel(self.existing_labels)
        self.labels_view.setSelectionMode(QListView.SingleSelection)
        self.labels_view.setItemDelegate(CapitalizeDelegate(self))
        self.control_layout.addWidget(self.labels_view)

        self.new_label_txt = QLineEdit(self)
        self.new_label_txt.returnPressed.connect(self.add_new_label)
        self.new_label_txt.returnPressed.connect(self.new_label_txt.clear)
        self.control_layout.addWidget(self.new_label_txt)

        self.add_button = QPushButton('Add Label', self)
        self.add_button.clicked.connect(self.add_new_label)
        self.control_layout.addWidget(self.add_button)

        # Setup the training area
        train_box = QGroupBox('Train', self)
        train_box_layout = QVBoxLayout()
        train_box.setLayout(train_box_layout)
        self.control_layout.addWidget(train_box)
        self.train_btn = QPushButton('Train', self)
        self.train_btn.clicked.connect(self.train)
        train_box_layout.addWidget(self.train_btn)

        self.control_layout.addStretch(0)

        # Add take picture shortcut
        self.take_picture_btn = QPushButton('Take picture', self)
        self.take_picture_btn.clicked.connect(self.take_picture)
        self.control_layout.addWidget(self.take_picture_btn)
        shortcut = QShortcut(QKeySequence('Space'), self, self.take_picture)
        shortcut.setWhatsThis('Take picture and add to training data.')

        # Add quit shortcut
        shortcut = QShortcut(QKeySequence('Esc'), self, self.close)
        shortcut.setWhatsThis('Quit')

        # Setup the main camera area
        self.image_label = QLabel(self)
        self.image_label.setFixedSize(self.video_size)
        self.main_layout.addWidget(self.image_label)

        # Setup the camera
        self.capture = cv2.VideoCapture(0)
        self.capture.set(cv2.CAP_PROP_FRAME_WIDTH, self.video_size.width())
        self.capture.set(cv2.CAP_PROP_FRAME_HEIGHT, self.video_size.height())

        self.timer = QTimer()
        self.timer.timeout.connect(self.display_video_stream)
        self.timer.start(int(1000 / self.fps))

    def classify_face(self, image):
        if self.model is None:
            return

        label_idx, distances = self.model.predict(image.ravel(), True)
        label_idx, distance = label_idx[0], distances[0][label_idx]

        labels = self.existing_labels.stringList()
        return labels[label_idx], distance

    def get_training_data(self):
        """Read the images from disk into an n*(w*h) matrix."""
        return data_provider.get_image_data_from_directory(
            self.training_data_dir)

    def train(self):
        X, y, mapping = self.get_training_data()
        # Inspect scree plot to determine appropriate number of PCA components
        classifier = PCALDAClassifier(
            n_components=2, pca_components=200, metric='euclidean',
        ).fit(X, y)

        # Replace the existing running model
        self.model = classifier

        # Save the classifier to file
        data_provider.save_model(
            classifier, path.join(self.models_dir, self.model_fname))

    def add_new_label(self):
        new_label = self.new_label_txt.text()
        new_label = new_label.lower()

        # Prevent empty entries
        if len(new_label) < 3:
            return

        string_list = self.existing_labels.stringList()

        if new_label not in string_list:
            string_list.append(new_label)
            string_list.sort()
            self.existing_labels.setStringList(string_list)

            # Automatically select the added label
            selection_model = self.labels_view.selectionModel()
            index = self.existing_labels.index(string_list.index(new_label))
            selection_model.setCurrentIndex(index, QItemSelectionModel.Select)

    def display_video_stream(self):
        """Read frame from camera and repaint QLabel widget."""
        _, frame = self.capture.read()
        frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
        frame = cv2.flip(frame, 1)

        # Use the Viola-Jones face detector to detect faces to classify
        face_cascade = cv2.CascadeClassifier(path.join(
            self.pkg_path, 'resources', 'haarcascade_frontalface_default.xml'))
        self.gray_image = gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)

        self.detected_faces = face_cascade.detectMultiScale(gray, 1.3, 5)
        for x, y, w, h in self.detected_faces:
            # Label the detected face as per the model
            face = gray[y:y + h, x:x + w]
            face = cv2.resize(face, self.IMAGE_SIZE)

            result = self.classify_face(face)
            # If a model is loaded, we can predict
            if result:
                predicted, distance = self.classify_face(face)

                if distance > self.STRANGER_DANGER:
                    predicted = 'Stranger danger!'
                    color = self.stranger_color
                else:
                    predicted = predicted.capitalize()
                    color = self.recognized_color

                # Draw a rectangle around the detected face
                cv2.rectangle(frame, (x, y), (x + w, y + h), color, 2)
                text = '%s (%.1f)' % (predicted, distance)
                cv2.putText(frame, text, (x, y + h + 15),
                            cv2.FONT_HERSHEY_TRIPLEX, 0.5, color)
            else:
                # Draw a rectangle around the detected face
                cv2.rectangle(frame, (x, y), (x + w, y + h),
                              self.stranger_color, 2)
                cv2.putText(frame, 'Stranger danger!', (x, y + h + 15),
                            cv2.FONT_HERSHEY_TRIPLEX, 0.5, self.stranger_color)

        # Display the image in the image area
        image = QImage(frame, frame.shape[1], frame.shape[0],
                       frame.strides[0], QImage.Format_RGB888)
        self.image_label.setPixmap(QPixmap.fromImage(image))

    @contextmanager
    def stop_camera_feed(self):
        """Temporarly stop the feed and face detection."""
        try:
            self.timer.stop()
            yield
        finally:
            self.timer.start(int(1000 / self.fps))

    def take_picture(self):
        # Notify the user there were no faces detected
        if self.detected_faces is None or len(self.detected_faces) < 1:
            return
            raise NoFacesError()

        if len(self.detected_faces) > 1:
            return
            raise MultipleFacesError()

        with self.stop_camera_feed():
            x, y, w, h = self.detected_faces[0]

            face = self.gray_image[y:y + h, x:x + w]
            face = cv2.resize(face, self.IMAGE_SIZE)
            denoised_image = cv2.fastNlMeansDenoising(face)

            if not self.selected_label:
                return

            self.save_image(denoised_image, self.selected_label)

    @property
    def selected_label(self):
        index = self.labels_view.selectedIndexes()
        if len(index) < 1:
            return None

        label = self.existing_labels.data(index[0], Qt.DisplayRole)

        return label

    def get_existing_labels(self):
        """Get a list of the currently existing labels"""
        return data_provider.get_folder_names(self.training_data_dir)

    def save_image(self, image: np.ndarray, label: str) -> None:
        """Save an image to disk in the appropriate directory."""
        if not path.exists(self.training_data_dir):
            mkdir(self.training_data_dir)

        label_path = path.join(self.training_data_dir, label)
        if not path.exists(label_path):
            mkdir(label_path)

        existing_files = listdir(label_path)
        existing_files = map(lambda p: path.splitext(p)[0], existing_files)
        existing_files = list(map(int, existing_files))
        last_fname = sorted(existing_files)[-1] if len(existing_files) else 0

        fname = path.join(label_path, '%03d.png' % (last_fname + 1))
        cv2.imwrite(fname, image)