class AudioTest(QMainWindow): PUSH_MODE_LABEL = "Enable push mode" PULL_MODE_LABEL = "Enable pull mode" SUSPEND_LABEL = "Suspend playback" RESUME_LABEL = "Resume playback" DurationSeconds = 1 ToneSampleRateHz = 600 DataSampleRateHz = 44100 def __init__(self): super(AudioTest, self).__init__() self.m_device = QAudioDeviceInfo.defaultOutputDevice() self.m_output = None self.initializeWindow() self.initializeAudio() def initializeWindow(self): layout = QVBoxLayout() self.m_deviceBox = QComboBox(activated=self.deviceChanged) for deviceInfo in QAudioDeviceInfo.availableDevices( QAudio.AudioOutput): self.m_deviceBox.addItem(deviceInfo.deviceName(), deviceInfo) layout.addWidget(self.m_deviceBox) self.m_modeButton = QPushButton(clicked=self.toggleMode) self.m_modeButton.setText(self.PUSH_MODE_LABEL) layout.addWidget(self.m_modeButton) self.m_suspendResumeButton = QPushButton( clicked=self.toggleSuspendResume) self.m_suspendResumeButton.setText(self.SUSPEND_LABEL) layout.addWidget(self.m_suspendResumeButton) volumeBox = QHBoxLayout() volumeLabel = QLabel("Volume:") self.m_volumeSlider = QSlider(Qt.Horizontal, minimum=0, maximum=100, singleStep=10, valueChanged=self.volumeChanged) volumeBox.addWidget(volumeLabel) volumeBox.addWidget(self.m_volumeSlider) layout.addLayout(volumeBox) window = QWidget() window.setLayout(layout) self.setCentralWidget(window) def initializeAudio(self): self.m_pullTimer = QTimer(self, timeout=self.pullTimerExpired) self.m_pullMode = True self.m_format = QAudioFormat() self.m_format.setSampleRate(self.DataSampleRateHz) self.m_format.setChannelCount(1) self.m_format.setSampleSize(16) self.m_format.setCodec('audio/pcm') self.m_format.setByteOrder(QAudioFormat.LittleEndian) self.m_format.setSampleType(QAudioFormat.SignedInt) info = QAudioDeviceInfo(QAudioDeviceInfo.defaultOutputDevice()) if not info.isFormatSupported(self.m_format): qWarning("Default format not supported - trying to use nearest") self.m_format = info.nearestFormat(self.m_format) self.m_generator = Generator(self.m_format, self.DurationSeconds * 1000000, self.ToneSampleRateHz, self) self.createAudioOutput() def createAudioOutput(self): self.m_audioOutput = QAudioOutput(self.m_device, self.m_format) self.m_audioOutput.notify.connect(self.notified) self.m_audioOutput.stateChanged.connect(self.handleStateChanged) self.m_generator.start() self.m_audioOutput.start(self.m_generator) self.m_volumeSlider.setValue(self.m_audioOutput.volume() * 100) def deviceChanged(self, index): self.m_pullTimer.stop() self.m_generator.stop() self.m_audioOutput.stop() self.m_device = self.m_deviceBox.itemData(index) self.createAudioOutput() def volumeChanged(self, value): if self.m_audioOutput is not None: self.m_audioOutput.setVolume(value / 100.0) def notified(self): qWarning( "bytesFree = %d, elapsedUSecs = %d, processedUSecs = %d" % (self.m_audioOutput.bytesFree(), self.m_audioOutput.elapsedUSecs(), self.m_audioOutput.processedUSecs())) def pullTimerExpired(self): if self.m_audioOutput is not None and self.m_audioOutput.state( ) != QAudio.StoppedState: chunks = self.m_audioOutput.bytesFree( ) // self.m_audioOutput.periodSize() for _ in range(chunks): data = self.m_generator.read(self.m_audioOutput.periodSize()) if data is None or len( data) != self.m_audioOutput.periodSize(): break self.m_output.write(data) def toggleMode(self): self.m_pullTimer.stop() self.m_audioOutput.stop() if self.m_pullMode: self.m_modeButton.setText(self.PULL_MODE_LABEL) self.m_output = self.m_audioOutput.start() self.m_pullMode = False self.m_pullTimer.start(20) else: self.m_modeButton.setText(self.PUSH_MODE_LABEL) self.m_pullMode = True self.m_audioOutput.start(self.m_generator) self.m_suspendResumeButton.setText(self.SUSPEND_LABEL) def toggleSuspendResume(self): if self.m_audioOutput.state() == QAudio.SuspendedState: qWarning("status: Suspended, resume()") self.m_audioOutput.resume() self.m_suspendResumeButton.setText(self.SUSPEND_LABEL) elif self.m_audioOutput.state() == QAudio.ActiveState: qWarning("status: Active, suspend()") self.m_audioOutput.suspend() self.m_suspendResumeButton.setText(self.RESUME_LABEL) elif self.m_audioOutput.state() == QAudio.StoppedState: qWarning("status: Stopped, resume()") self.m_audioOutput.resume() self.m_suspendResumeButton.setText(self.SUSPEND_LABEL) elif self.m_audioOutput.state() == QAudio.IdleState: qWarning("status: IdleState") stateMap = { QAudio.ActiveState: "ActiveState", QAudio.SuspendedState: "SuspendedState", QAudio.StoppedState: "StoppedState", QAudio.IdleState: "IdleState" } def handleStateChanged(self, state): qWarning("state = " + self.stateMap.get(state, "Unknown"))
class MayRenderer(QWidget): shouldSave = pyqtSignal() texsize = 512 samplerate = 44100 shaderHeader = '#version 130\nuniform float iTexSize;\nuniform float iBlockOffset;\nuniform float iSampleRate;\n\n' def __init__(self, parent): super().__init__() self.parent = parent self.blocksize = (self.texsize * self.texsize) / self.samplerate self.initState() self.initUI() self.initAudio() def initState(self): self.playing = False self.initVolume = 1 self.useWatchFile = False self.watchFileName = '' self.storeCodeIfNotWatching = '' self.useSynDump = False self.synDrumName = '' self.synFileName = '' def initUI(self): self.mainLayout = QVBoxLayout() self.codeLayout = QVBoxLayout() self.codeButtonBar = QHBoxLayout() self.codeWatchFileBar = QHBoxLayout() self.renderBar = QHBoxLayout() self.playbackBar = QHBoxLayout() self.renderGroup = QGroupBox() self.renderGroupLayout = QVBoxLayout() self.renderGroupLayout.addLayout(self.renderBar) self.renderGroupLayout.addLayout(self.playbackBar) self.renderGroup.setLayout(self.renderGroupLayout) self.renderGroup.setObjectName("renderGroup") self.synGroupLayout = QHBoxLayout() self.synGroup = QGroupBox() self.synGroup.setLayout(self.synGroupLayout) self.synDumpCheckBox = QCheckBox('Dump as') self.synDrumNameBox = QLineEdit(self) self.synFileNameBox = QLineEdit(self) self.synFileButton = QPushButton('...') self.synGroupLayout.addWidget(self.synDumpCheckBox, 1) self.synGroupLayout.addWidget(self.synDrumNameBox, 2) self.synGroupLayout.addWidget(QLabel('in'), .1) self.synGroupLayout.addWidget(self.synFileNameBox, 5) self.synGroupLayout.addWidget(self.synFileButton, 0.1) self.synDumpCheckBox.stateChanged.connect(self.toggleSynDump) self.synDrumNameBox.setPlaceholderText('drumname') self.synDrumNameBox.textChanged.connect(self.setSynDrumName) self.synFileNameBox.setPlaceholderText('some aMaySyn .syn file') self.synFileNameBox.textChanged.connect(self.setSynFileName) self.synFileButton.setMaximumWidth(40) self.synFileButton.clicked.connect(self.chooseSynFile) self.codeGroup = QGroupBox() self.buttonCopy = QPushButton('↬ Clipboard', self) self.buttonCopy.clicked.connect(self.copyToClipboard) self.buttonPaste = QPushButton('Paste ↴', self) self.buttonPaste.clicked.connect(self.pasteClipboard) self.buttonClear = QPushButton('×', self) self.buttonClear.clicked.connect(self.clearEditor) self.codeEditor = QPlainTextEdit(self) self.codeEditor.setLineWrapMode(QPlainTextEdit.WidgetWidth) #self.codeEditor.setCenterOnScroll(True) #self.codeEditor.textChanged.formatEditor()) # this gives a recursion problem, but how to filter e.g. tabs? self.codeEditor.cursorPositionChanged.connect(self.updatePosLabel) self.codeEditor.setTabStopWidth(14) self.watchFileCheckBox = QCheckBox('watch file:', self) self.watchFileCheckBox.stateChanged.connect(self.toggleWatchFile) self.watchFileNameBox = QLineEdit(self) self.watchFileNameBox.setPlaceholderText( 'use GLSL code file instead of the above editor...') self.watchFileNameBox.textChanged.connect(self.setWatchFileName) self.buttonWatchFile = QPushButton('...', self) self.buttonWatchFile.setMaximumWidth(40) self.buttonWatchFile.clicked.connect(self.chooseWatchFile) self.renderButton = QPushButton(self) self.renderButton.clicked.connect(self.pressRenderShader) self.renderLengthBox = QDoubleSpinBox(self) self.renderLengthBox.setMinimum(0) self.renderLengthBox.setValue(4 * self.blocksize - .01) self.renderLengthBox.setSingleStep(self.blocksize) self.renderLengthBox.setSuffix(' sec') self.renderLengthBox.setToolTip('render length') self.renderBpmBox = QSpinBox(self) self.renderBpmBox.setRange(1, 999) self.renderBpmBox.setValue(160) self.renderBpmBox.setPrefix('BPM ') self.renderBpmBox.setToolTip('--> determines SPB') self.playbackVolumeSlider = QSlider(Qt.Horizontal) self.playbackVolumeSlider.setMaximum(100) self.playbackVolumeSlider.setValue(self.initVolume * 100) self.playbackVolumeSlider.setToolTip('volume') self.playbackVolumeSlider.sliderMoved.connect(self.setVolume) self.renderBar.addWidget(self.renderButton, 60) self.renderBar.addWidget(self.renderBpmBox, 20) self.renderBar.addWidget(self.renderLengthBox, 20) self.progressBar = QProgressBar(self) self.progressBar.setEnabled(False) self.pauseButton = QPushButton(self) self.pauseButton.setEnabled(False) self.pauseButton.clicked.connect(self.pressPauseButton) self.playbackBar.addWidget(self.progressBar, 60) self.playbackBar.addWidget(self.playbackVolumeSlider, 20) self.playbackBar.addWidget(self.pauseButton, 20) self.codeButtonBar.addWidget(self.buttonCopy) self.codeButtonBar.addWidget(self.buttonPaste) self.codeButtonBar.addWidget(self.buttonClear) self.codeWatchFileBar.addWidget(self.watchFileCheckBox) self.codeWatchFileBar.addWidget(self.watchFileNameBox) self.codeWatchFileBar.addWidget(self.buttonWatchFile) self.codeHeader = QHBoxLayout() self.codePosLabel = QLabel('(0,0)') self.codeHeader.addWidget(QLabel('GLSL code')) self.codeHeader.addStretch() self.codeHeader.addWidget(self.codePosLabel) self.codeLayout.addLayout(self.codeHeader) self.codeLayout.addLayout(self.codeButtonBar) self.codeLayout.addWidget(self.codeEditor) self.codeLayout.addLayout(self.codeWatchFileBar) self.codeGroup.setLayout(self.codeLayout) self.mainLayout.addWidget(self.synGroup) self.mainLayout.addWidget(self.codeGroup) self.mainLayout.addWidget(self.renderGroup) self.updatePlayingUI() self.setLayout(self.mainLayout) def updatePlayingUI(self, keepActive=False): self.renderButton.setText( 'shut the f**k up' if self.playing else 'send to hell') if not self.playing and not keepActive: self.progressBar.setValue(0) self.progressBar.setEnabled(self.playing if not keepActive else True) self.pauseButton.setEnabled(self.playing if not keepActive else True) self.pauseButton.setText('||' if ( self.playing and self.audiooutput.state() != QAudio.SuspendedState ) else '▶') 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.audiodeviceinfo = QAudioDeviceInfo(QAudioDeviceInfo.defaultOutputDevice()) self.audiooutput = QAudioOutput(self.audioformat) self.audiooutput.setVolume(self.initVolume) def paste(self, source): self.codeEditor.clear() source = source.replace(4 * ' ', '\t').replace(3 * ' ', '\t') self.codeEditor.insertPlainText(source) #self.codeEditor.setFocus() #TODO: think about whether we want this self.codeEditor.ensureCursorVisible() def pasteClipboard(self): self.paste(self.shaderHeader + QApplication.clipboard().text()) def copyToClipboard(self): text = self.codeEditor.toPlainText().replace('\t', 4 * ' ') QApplication.clipboard().setText(text) def clearEditor(self): self.codeEditor.setPlainText('') self.codeEditor.setFocus() def updatePosLabel(self): cursor = self.codeEditor.textCursor() self.codePosLabel.setText( f'({cursor.blockNumber()},{cursor.positionInBlock()})') # def formatEditor(self): # plainText = self.codeEditor.toPlainText().replace('\t', 4*' ') # self.codeEditor.setPlainText(plainText) def toggleSynDump(self, state): self.useSynDump = (state == Qt.Checked) self.shouldSave.emit() def chooseSynFile(self): dialogResult, _ = QFileDialog.getSaveFileName( self, 'Choose SYN definition file', '', 'aMaySyn definition files (*.syn);;All files (*)') print(dialogResult) self.synFileNameBox.setText(dialogResult) self.synDumpCheckBox.setCheckState(Qt.Checked) if self.synFileName == '': self.synFileNameBox.setFocus() self.shouldSave.emit() def setSynFileName(self): self.synFileName = self.synFileNameBox.text() self.shouldSave.emit() def setSynDrumName(self): self.synDrumName = self.synDrumNameBox.text() self.shouldSave.emit() def setSynDumpParameters(self, useSynDump, synFileName, synDrumName): self.synDumpCheckBox.setChecked(useSynDump) self.synFileNameBox.setText(synFileName) self.synDrumNameBox.setText(synDrumName) def toggleWatchFile(self, state): if not self.useWatchFile and state == Qt.Checked: self.storeCodeIfNotWatching = self.codeEditor.toPlainText() self.useWatchFile = (state == Qt.Checked) self.codeEditor.setEnabled(not self.useWatchFile) if self.useWatchFile: self.showWatchFileInfo() else: self.codeEditor.setPlainText(self.storeCodeIfNotWatching) def chooseWatchFile(self): dialogResult = QFileDialog.getOpenFileName( self, 'Choose file with GLSL code', '', 'GLSL files (*.glsl);;All files (*)') print(dialogResult) self.watchFileNameBox.setText(dialogResult[0]) self.watchFileCheckBox.setCheckState(Qt.Checked) self.shouldSave.emit() def setWatchFileName(self): self.watchFileName = self.watchFileNameBox.text() self.showWatchFileInfo() def showWatchFileInfo(self): if self.useWatchFile: fileInfo = QFileInfo(self.watchFileName) infoText = 'use code from file:\n' + self.watchFileName + '\n' + ( '(exists)' if fileInfo.exists() else '(doesn\'t exist)') self.codeEditor.setPlainText(infoText) def pressRenderShader(self): self.playing = not self.playing if self.playing: self.renderShaderAndPlay() else: self.stopShader() def pressPauseButton(self): state = self.audiooutput.state() if state == QAudio.ActiveState: self.audiooutput.suspend() elif state == QAudio.SuspendedState: self.audiooutput.resume() self.updatePlayingUI(keepActive=True) def stopShader(self): self.audiooutput.stop() self.updatePlayingUI() def renderShaderAndPlay(self, file=None): self.playing = True self.updatePlayingUI() shaderSource = self.shaderHeader + """ uniform float SPB; void main() { float t = (iBlockOffset + gl_FragCoord.x + gl_FragCoord.y*iTexSize) / iSampleRate; t = floor(t*BITS.) / BITS.; vec2 s = .2 * vec2(sin(2.*3.14159*49.*t*(1.+t)*SPB*2.667)); // let's make it fun and squeaky vec2 v = floor((0.5+0.5*s)*65535.0); vec2 vl = mod(v,256.0)/255.0; vec2 vh = floor(v/256.0)/255.0; gl_FragColor = vec4(vl.x,vh.x,vl.y,vh.y); } """ # this is the SUPER FUN BITCRUSHER for the test shader nr_bits = randint(128, 8192) shaderSource = shaderSource.replace('BITS', str(nr_bits)) print(nr_bits, 'bits for the SUPER FUN BITCRUSHER in the test shader.') starttime = datetime.now() try: if self.useWatchFile: watchFile = QFile(self.watchFileName) if not watchFile.open(QFile.ReadOnly | QFile.Text): QMessageBox.warning( self, "Öhm... blöd.", "File öffnen ging nicht. Is genügend Pfeffer drauf?") self.playing = False self.updatePlayingUI() return textStream = QTextStream(watchFile) textStream.setCodec('utf-8') shaderSource = self.shaderHeader + textStream.readAll() else: code = self.codeEditor.toPlainText() if code: shaderSource = code except: raise uniforms = {} SPB = 60 / float(self.renderBpmBox.value()) uniforms.update({'SPB': SPB}) print(self.renderLengthBox.value()) try: duration = self.renderLengthBox.value() except: print('couldn\'t read duration field. take 10secs.') duration = 10 glwidget = SFXGLWidget(self, self.audioformat.sampleRate(), duration, self.texsize, moreUniforms=uniforms) glwidget.show() log = glwidget.newShader(shaderSource) print(log) self.music = glwidget.music floatmusic = glwidget.floatmusic glwidget.hide() glwidget.destroy() if self.music == None: return self.renderLengthBox.setValue(round(glwidget.duration_real, 2) - .01) self.bytearray = QByteArray(self.music) self.audiobuffer = QBuffer(self.bytearray) self.audiobuffer.open(QIODevice.ReadOnly) endtime = datetime.now() el = endtime - starttime print("Compile time: {:.3f}s".format(el.total_seconds())) self.audiooutput.stop() self.audiooutput.start(self.audiobuffer) self.audiooutput.setNotifyInterval(100) self.audiooutput.stateChanged.connect(self.updatePlayingUI) self.progressBar.setMaximum(self.audiobuffer.size()) self.audiooutput.notify.connect(self.proceedAudio) if file is not None: floatmusic_L = [] floatmusic_R = [] for n, sample in enumerate(floatmusic): if n % 2 == 0: floatmusic_L.append(sample) else: floatmusic_R.append(sample) floatmusic_stereo = np.transpose( np.array([floatmusic_L, floatmusic_R], dtype=np.float32)) wavfile.write(file, self.samplerate, floatmusic_stereo) def proceedAudio(self): # print(self.audiobuffer.pos() / self.audioformat.sampleRate()) self.progressBar.setValue(self.audiobuffer.pos()) if self.audiobuffer.atEnd(): self.audiooutput.stop() self.playing = False self.updatePlayingUI() def setVolume(self): self.audiooutput.setVolume(self.playbackVolumeSlider.value() * .01) def dumpInSynFile(self, drumatizeL, drumatizeR, envCode, releaseTime): if not self.synDumpCheckBox.isChecked(): return if self.synDrumName == '': print("specify a valid drum name!!") return if self.synFileName == '': print("specify a valid .syn filename!!") return if not path.exists(self.synFileName): open(self.synFileName, 'a').close() uniqueEnv = f'_{self.synDrumName}ENV' drumatizeL = drumatizeL.replace('__ENV', uniqueEnv) drumatizeR = drumatizeR.replace('__ENV', uniqueEnv) envCode = envCode.replace('__ENV', uniqueEnv).replace('\n', ' ') parLine = f'param include src="{envCode}"\n' synLine = f'maindrum {self.synDrumName} src="{drumatizeL}" srcr="{drumatizeR}" release={releaseTime}\n' print(parLine, '\n', synLine) tmpSynFile = 'tmp.syn' copyfile(self.synFileName, tmpSynFile) parWritten, synWritten = False, False with open(tmpSynFile, 'r') as synFileHandle: synFileLines = synFileHandle.readlines() with open(self.synFileName, 'w') as synFileHandle: for line in synFileLines: parseLine = line.strip('\n').split() if parseLine[0:2] == ['maindrum', self.synDrumName]: synFileHandle.write(synLine) synWritten = True elif parseLine[0:2] == ['param', 'include' ] and line.find(uniqueEnv) != -1: synFileHandle.write(parLine) parWritten = True else: synFileHandle.write(line) if not parWritten: synFileHandle.write('\n' + parLine) if not synWritten: synFileHandle.write('\n' + synLine) synFileHandle.close()
class AudioTest(QMainWindow): PUSH_MODE_LABEL = "Enable push mode" PULL_MODE_LABEL = "Enable pull mode" SUSPEND_LABEL = "Suspend playback" RESUME_LABEL = "Resume playback" DurationSeconds = 1 ToneSampleRateHz = 600 DataSampleRateHz = 44100 def __init__(self): super(AudioTest, self).__init__() self.m_device = QAudioDeviceInfo.defaultOutputDevice() self.m_output = None self.initializeWindow() self.initializeAudio() def initializeWindow(self): layout = QVBoxLayout() self.m_deviceBox = QComboBox(activated=self.deviceChanged) for deviceInfo in QAudioDeviceInfo.availableDevices(QAudio.AudioOutput): self.m_deviceBox.addItem(deviceInfo.deviceName(), deviceInfo) layout.addWidget(self.m_deviceBox) self.m_modeButton = QPushButton(clicked=self.toggleMode) self.m_modeButton.setText(self.PUSH_MODE_LABEL) layout.addWidget(self.m_modeButton) self.m_suspendResumeButton = QPushButton( clicked=self.toggleSuspendResume) self.m_suspendResumeButton.setText(self.SUSPEND_LABEL) layout.addWidget(self.m_suspendResumeButton) volumeBox = QHBoxLayout() volumeLabel = QLabel("Volume:") self.m_volumeSlider = QSlider(Qt.Horizontal, minimum=0, maximum=100, singleStep=10, valueChanged=self.volumeChanged) volumeBox.addWidget(volumeLabel) volumeBox.addWidget(self.m_volumeSlider) layout.addLayout(volumeBox) window = QWidget() window.setLayout(layout) self.setCentralWidget(window) def initializeAudio(self): self.m_pullTimer = QTimer(self, timeout=self.pullTimerExpired) self.m_pullMode = True self.m_format = QAudioFormat() self.m_format.setSampleRate(self.DataSampleRateHz) self.m_format.setChannelCount(1) self.m_format.setSampleSize(16) self.m_format.setCodec('audio/pcm') self.m_format.setByteOrder(QAudioFormat.LittleEndian) self.m_format.setSampleType(QAudioFormat.SignedInt) info = QAudioDeviceInfo(QAudioDeviceInfo.defaultOutputDevice()) if not info.isFormatSupported(self.m_format): qWarning("Default format not supported - trying to use nearest") self.m_format = info.nearestFormat(self.m_format) self.m_generator = Generator(self.m_format, self.DurationSeconds * 1000000, self.ToneSampleRateHz, self) self.createAudioOutput() def createAudioOutput(self): self.m_audioOutput = QAudioOutput(self.m_device, self.m_format) self.m_audioOutput.notify.connect(self.notified) self.m_audioOutput.stateChanged.connect(self.handleStateChanged) self.m_generator.start() self.m_audioOutput.start(self.m_generator) self.m_volumeSlider.setValue(self.m_audioOutput.volume() * 100) def deviceChanged(self, index): self.m_pullTimer.stop() self.m_generator.stop() self.m_audioOutput.stop() self.m_device = self.m_deviceBox.itemData(index) self.createAudioOutput() def volumeChanged(self, value): if self.m_audioOutput is not None: self.m_audioOutput.setVolume(value / 100.0) def notified(self): qWarning("bytesFree = %d, elapsedUSecs = %d, processedUSecs = %d" % ( self.m_audioOutput.bytesFree(), self.m_audioOutput.elapsedUSecs(), self.m_audioOutput.processedUSecs())) def pullTimerExpired(self): if self.m_audioOutput is not None and self.m_audioOutput.state() != QAudio.StoppedState: chunks = self.m_audioOutput.bytesFree() // self.m_audioOutput.periodSize() for _ in range(chunks): data = self.m_generator.read(self.m_audioOutput.periodSize()) if data is None or len(data) != self.m_audioOutput.periodSize(): break self.m_output.write(data) def toggleMode(self): self.m_pullTimer.stop() self.m_audioOutput.stop() if self.m_pullMode: self.m_modeButton.setText(self.PULL_MODE_LABEL) self.m_output = self.m_audioOutput.start() self.m_pullMode = False self.m_pullTimer.start(20) else: self.m_modeButton.setText(self.PUSH_MODE_LABEL) self.m_pullMode = True self.m_audioOutput.start(self.m_generator) self.m_suspendResumeButton.setText(self.SUSPEND_LABEL) def toggleSuspendResume(self): if self.m_audioOutput.state() == QAudio.SuspendedState: qWarning("status: Suspended, resume()") self.m_audioOutput.resume() self.m_suspendResumeButton.setText(self.SUSPEND_LABEL) elif self.m_audioOutput.state() == QAudio.ActiveState: qWarning("status: Active, suspend()") self.m_audioOutput.suspend() self.m_suspendResumeButton.setText(self.RESUME_LABEL) elif self.m_audioOutput.state() == QAudio.StoppedState: qWarning("status: Stopped, resume()") self.m_audioOutput.resume() self.m_suspendResumeButton.setText(self.SUSPEND_LABEL) elif self.m_audioOutput.state() == QAudio.IdleState: qWarning("status: IdleState") stateMap = { QAudio.ActiveState: "ActiveState", QAudio.SuspendedState: "SuspendedState", QAudio.StoppedState: "StoppedState", QAudio.IdleState: "IdleState"} def handleStateChanged(self, state): qWarning("state = " + self.stateMap.get(state, "Unknown"))
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()
class AudioAnalysis(QDialog): def __init__(self, mainwin, dir): super().__init__() self.main_Win = mainwin self.snd_record_ctr = 0 self.snd_play_ctr = 0 self.snd_reset_ctr = 0 self.is_snd_recording = None # self.audio = Audio(save_dir=dir) self.initAUD() self.initIF() self.initWaveList() def initIF(self): self.layout = QGridLayout(self) self.setLayout(self.layout) icon = QIcon() icon.addPixmap(QPixmap("./style/logo3.png")) self.setWindowIcon(icon) self.setWindowTitle("语音录制与分析") self.list_LW = QListWidget(self) self.list_LW.setMaximumWidth(160) class WaveSpectrum(QWidget): def __init__(self, parent=None, maindlg=None): super(WaveSpectrum, self).__init__(parent) self.main_Dlg = maindlg #self.pg_PL = pg.PlotWidget(enableMenu=False) self.audio = self.main_Dlg.audio self.layout = QGridLayout(self) self.setLayout(self.layout) self.pg_PL = pg.PlotWidget() #pg.plot(title="Three plot curves") self.pg_PL.hideButtons() self.layout.addWidget(self.pg_PL) self.item = self.pg_PL.getPlotItem() self.item.hideButtons() self.item.setMouseEnabled(y=False) self.item.setYRange(0,20000) range = self.audio.rate/2 self.item.setXRange(-range,range, padding=0) self.axis = self.item.getAxis("bottom") self.axis.setLabel("频率(赫兹)") def updatePlot(self): try: data = np.fromstring(self.audio.block, 'int16') #print(data) T = 1.0/self.audio.rate N = data.shape[0] Fx = (1./N) * np.fft.fft(data) # 万一N==0 except Exception as e: print("??",e) else: f = np.fft.fftfreq(N, T) Fx = np.fft.fftshift(Fx) f = np.fft.fftshift(f) self.item.plot(x=f.tolist(), y=(np.absolute(Fx)).tolist(), clear=True) self.wave_spectrum_PG = WaveSpectrum(maindlg=self) self.result_LB = QLabel(self) self.result_LB.setText("欢迎使用") self.running_SL = QSlider(Qt.Horizontal) self.running_SL.setMinimum(0) self.running_SL.setMaximum(100) self.running_SL.setStyleSheet("QSlider::handle:horizontal {background-color: #d91900;}") self.save_BT = QPushButton(self) self.save_BT.setText("保存与分析") self.save_BT.setMinimumSize(128,32) self.record_BT = QPushButton(self) self.record_BT.setText("开始录音") self.record_BT.setMinimumSize(144,32) self.play_BT = QPushButton(self) self.play_BT.setText("开始播放") self.play_BT.setMinimumSize(144,32) self.reset_BT = QPushButton(self) self.reset_BT.setText("停止") self.reset_BT.setMinimumSize(128,32) self.layout.addWidget(self.list_LW, 0,0,1,1) self.layout.addWidget(self.wave_spectrum_PG, 0,1, 1,3) self.layout.addWidget(self.result_LB, 1,0, 1,4) self.layout.addWidget(self.running_SL, 2,0, 1,4) self.layout.addWidget(self.save_BT, 3,0, 2,1) self.layout.addWidget(self.record_BT, 3,1, 2,1) self.layout.addWidget(self.play_BT, 3,2, 2,1) self.layout.addWidget(self.reset_BT, 3,3, 2,1) self.list_LW.itemClicked.connect(self.sel2Play) self.record_BT.clicked.connect(self.click2Record) self.running_SL.sliderReleased.connect(self.dragPosPlay) # 注意这里得是用户主动的动作哟 另外如果需要点击位置定位的话还必须要重写mousePressEvent,这里就不弄了 self.play_BT.clicked.connect(self.click2Play) self.reset_BT.clicked.connect(self.click2Reset) self.save_BT.clicked.connect(self.click2Save) def initWaveList(self): self.wave_dict = {"小黄":["catH1.wav",0],"小黄骚":["catH2.wav",0], "小黄又骚":["catH3.wav",0], "小黄又又骚":["catH4.wav",0] ,"煤球":["catM1.wav",0],"煤球骚":["catM2.wav",0], "煤球又骚":["catM3.wav",0] ,"老公":["laog.wav",0], "老婆":["laop.wav",0]} for k in self.wave_dict: item = QListWidgetItem() item.setText(k) item.setData(Qt.UserRole, self.wave_dict[k]) self.list_LW.addItem(item) def initAUD(self): # info = QAudioDeviceInfo.defaultInputDevice() if (~info.isFormatSupported(self.audio.format)): # print("警告,设置的默认音频格式并不支持,将尝试采用最相近的支持格式") # 不知道这里面有什么神改动? self.audio.format = info.nearestFormat(self.audio.format) # update_interval = 160 self.audioRecorder = QAudioInput(self.audio.format) self.audioRecorder.setNotifyInterval(update_interval) #按毫秒ms 类似于QTimer的作用 self.audioRecorder.notify.connect(self.processAudioData) self.audioRecorder_TD = QThread() self.audioRecorder.moveToThread(self.audioRecorder_TD) self.audioRecorder_TD.started.connect(self.startRecord) self.audioRecorder.stateChanged.connect(self.recordStopped) # 总结来说线程只是一个容器,里面执行的循环要是没法结束,强制退出也不好操作 # 所以还是好好写好任务流然后发送信号比较合理 self.audioPlayer = QAudioOutput(self.audio.format) self.audioPlayer.setNotifyInterval(update_interval) self.audioPlayer.notify.connect(self.processAudioData) self.audioPlayer_TD = QThread() self.audioPlayer.moveToThread(self.audioPlayer_TD) self.audioPlayer_TD.started.connect(self.startPlay) self.audioPlayer.stateChanged.connect(self.playStopped) # def startRecord(self): self.audioRecorder.start(self.audio.record_buffer) # 独立出来主要就是为了传个参数进去 def click2Record(self): if self.snd_play_ctr != 0: self.audioPlayer.suspend() self.audioPlayer.stop() self.audio.play_buffer.close() self.running_SL.setValue(0) self.audioPlayer_TD.quit() self.snd_play_ctr = 0 self.play_BT.setText("开始播放") # self.is_snd_recording = True self.running_SL.setStyleSheet("QSlider::handle:horizontal {background-color: #d91900;}") self.running_SL.setValue(0) if self.snd_record_ctr == 0: self.audio.record_buffer.open(QIODevice.WriteOnly) self.audioRecorder_TD.start() #注意这里是分线程进行 self.record_BT.setText("暂停录音") self.reset_BT.setText("停止录音") elif self.snd_record_ctr % 2 == 1: self.audioRecorder.suspend() self.result_LB.setText("录音暂停") self.record_BT.setText("继续录音") else: # self.snd_record_ctr % 2 == 0: self.audioRecorder.resume() self.record_BT.setText("暂停录音") self.snd_record_ctr += 1 def recordStopped(self): if self.audioRecorder.state() == QAudio.StoppedState: #==2 #QAudio.IdleState: #==3; self.audioRecorder_TD.quit() def startPlay(self): self.audioPlayer.start(self.audio.play_buffer) def click2Play(self): if self.is_snd_recording == None: self.result_LB.setText("还没录音呢!!!") else: if self.snd_record_ctr % 2 == 1: self.audioRecorder.suspend() self.record_BT.setText("继续录音") self.snd_record_ctr += 1 # self.is_snd_recording = False self.running_SL.setStyleSheet("QSlider::handle:horizontal {background-color: #007ad9;}") if self.snd_play_ctr == 0: data = self.audio.record_buffer.data() self.audio.play_buffer.setData(data) self.audio.play_buffer.open(QIODevice.ReadOnly) # 要在关闭的情况下设置数据然后在以某种模式打开 self.audioPlayer_TD.start() self.running_SL.setValue(0) self.start_time = 0 # self.start_time = self.running_SL.value() / 100 * self.audio.duration # self.audioPlayer.setVolume(0.8) self.play_BT.setText("暂停播放") self.reset_BT.setText("停止播放") elif self.snd_play_ctr % 2 == 1: self.audioPlayer.suspend() self.result_LB.setText("播放暂停") self.play_BT.setText("继续播放") else: self.audioPlayer.resume() self.play_BT.setText("暂停播放") self.snd_play_ctr += 1 def playStopped(self): if self.audioPlayer.state() == QAudio.IdleState: #==3; #QAudio.StoppedState: #==2 self.audioPlayer.stop() self.running_SL.setValue(0) self.audio.play_buffer.close() self.audioPlayer_TD.quit() self.snd_play_ctr = 0 self.play_BT.setText("开始播放") def dragPosPlay(self): if self.is_snd_recording == None: self.running_SL.setValue(0) else: if self.is_snd_recording & (self.snd_record_ctr % 2 == 1): self.audioRecorder.suspend() self.record_BT.setText("继续录音") self.snd_record_ctr += 1 if (not self.is_snd_recording) & (self.snd_play_ctr % 2 == 1): self.audioPlayer.suspend() self.play_BT.setText("继续播放") self.snd_play_ctr += 1 self.is_snd_recording = False self.running_SL.setStyleSheet("QSlider::handle:horizontal {background-color: #007ad9;}") self.audioPlayer.stop() self.audio.play_buffer.close() self.audioPlayer_TD.quit() # data = self.audio.record_buffer.data() self.audio.play_buffer.setData(data) self.audio.play_buffer.open(QIODevice.ReadOnly) # 要在关闭的情况下设置数据然后在以某种模式打开 self.audioPlayer_TD.start() data_size = self.audio.record_buffer.data().size() sel_pcent = self.running_SL.value() / 100 sel_size = int(sel_pcent * data_size) self.audio.pos = int(sel_pcent * (data_size / self.audio.chunksize)) # 重设第几个chunk开始播放 self.start_time = int(sel_pcent * self.audio.duration) # 重设开始播放时间 self.audio.play_buffer.seek(sel_size) self.snd_play_ctr = 1 self.play_BT.setText("暂停播放") def sel2Play(self, item): c0 = (self.is_snd_recording == None) c1 = ((self.is_snd_recording == False) & (self.snd_play_ctr % 2 == 0)) c2 = ((self.is_snd_recording == True) & (self.snd_record_ctr % 2 == 0)) if (c0 | c1 | c2): self.cur_item = item sound_dir = "./sound/" #self.cur_wave = os.path.abspath(item.data(Qt.UserRole)[0]) self.cur_wave = item.data(Qt.UserRole)[0] sound_path = os.path.join(sound_dir, self.cur_wave) with wave.open(sound_path, 'rb') as wf: data = wf.readframes(wf.getnframes()) self.audio.play_buffer.setData(data) self.audio.play_buffer.open(QIODevice.ReadOnly) self.start_time = 0 self.audio.duration = 10 # 随便给了个值,避免产生除0的问题其他没啥用 self.audioPlayer_TD.start() def click2Reset(self): if self.is_snd_recording == None: self.result_LB.setText("还没录音呢!!!") elif self.is_snd_recording: self.audioRecorder.stop() self.audio.record_buffer = QBuffer() self.snd_record_ctr = 0 self.result_LB.setText("录音停止") self.record_BT.setText("开始录音") else: #not self.is_snd_recording: self.audioPlayer.stop() self.audio.pos = 0 self.snd_play_ctr = 0 self.result_LB.setText("播放停止") self.play_BT.setText("开始播放") self.running_SL.setValue(0) def click2Save(self): if self.is_snd_recording == None: self.result_LB.setText("还没录音呢!!!") elif self.is_snd_recording: self.audioRecorder.suspend() else: self.audioPlayer.suspend() #self.audio.save_path = QFileDialog().getSaveFileName(self.main_Dlg, "选个保存的地方吧", new_path)[0] # 注意末尾那个[0]别丢了,不然返回的是tuple类型 self.audio.saveWave() self.snd_record_ctr = 0 self.result_LB.setText("录音存于:{};刚刚应该是{}叫了:)".format(os.path.abspath(self.audio.save_path), self.getMinDist())) self.record_BT.setText("开始录音") def processAudioData(self): if self.is_snd_recording: #self.audioRecorder.state() == QAudio.ActiveState: self.audio.block = self.audio.record_buffer.data().right(self.audio.chunksize) self.audio.duration = self.audioRecorder.processedUSecs() # 注意这里是微秒!!! interval = 10 self.running_SL.setValue((self.audio.duration / 1000000) % interval * (100 / interval)) show_info = "已录制{:.1f}秒".format(self.audio.duration/1000000.0) self.result_LB.setText(show_info) else: # self.audioPlayer.state() == QAudio.ActiveState: # 试过chop 不过好像没有必要 self.audio.block = self.audio.play_buffer.data().mid(self.audio.pos*self.audio.chunksize, self.audio.chunksize) self.audio.pos += 1 self.running_SL.setValue((self.start_time + self.audioPlayer.processedUSecs())/self.audio.duration*100) show_info = "正在播放{:.1f}/{:.1f}秒".format((self.start_time + self.audioPlayer.processedUSecs())/1000000.0 ,self.audio.duration/1000000.0) self.result_LB.setText(show_info) self.wave_spectrum_PG.updatePlot() def getMFCC(self,path): (rate, sig) = scwav.read(path) mfcc_feature = mfcc(sig, rate) nmfcc = np.array(mfcc_feature) y, sr = librosa.load(path) return librosa.feature.mfcc(y, sr) def compareMFCC(self, demo_path): mfcc1 = self.getMFCC(self.audio.save_path) print(demo_path) mfcc2 = self.getMFCC(demo_path) norm = lambda x, y: nlnorm(x-y, ord=1) d, cost_matrix, acc_cost_matrix, path = dtw(mfcc1.T, mfcc2.T, dist=norm) return d def getMinDist(self): i = 1000000 sound_dir = "./sound/" for k in self.wave_dict: self.wave_dict[k][1] = self.compareMFCC(os.path.join(sound_dir, self.wave_dict[k][0])) print("{}:{:.1f}".format(k, self.wave_dict[k][1])) i = min(i, self.wave_dict[k][1]) if i == self.wave_dict[k][1]: min_k = k return min_k
class Window(QWidget): def __init__(self, parent=None): QWidget.__init__(self, parent) format = QAudioFormat() format.setChannelCount(1) format.setSampleRate(22050) format.setSampleSize(16) format.setCodec("audio/pcm") format.setByteOrder(QAudioFormat.LittleEndian) format.setSampleType(QAudioFormat.SignedInt) self.output = QAudioOutput(format, self) self.frequency = 440 self.volume = 0 self.buffer = QBuffer() self.data = QByteArray() self.deviceLineEdit = QLineEdit() self.deviceLineEdit.setReadOnly(True) self.deviceLineEdit.setText( QAudioDeviceInfo.defaultOutputDevice().deviceName()) self.pitchSlider = QSlider(Qt.Horizontal) self.pitchSlider.setMaximum(100) self.volumeSlider = QSlider(Qt.Horizontal) self.volumeSlider.setMaximum(32767) self.volumeSlider.setPageStep(1024) self.playButton = QPushButton(self.tr("&Play")) self.pitchSlider.valueChanged.connect(self.changeFrequency) self.volumeSlider.valueChanged.connect(self.changeVolume) self.playButton.clicked.connect(self.play) formLayout = QFormLayout() formLayout.addRow(self.tr("Device:"), self.deviceLineEdit) formLayout.addRow(self.tr("P&itch:"), self.pitchSlider) formLayout.addRow(self.tr("&Volume:"), self.volumeSlider) buttonLayout = QVBoxLayout() buttonLayout.addWidget(self.playButton) buttonLayout.addStretch() horizontalLayout = QHBoxLayout(self) horizontalLayout.addLayout(formLayout) horizontalLayout.addLayout(buttonLayout) self.play() self.createData() def changeFrequency(self, value): self.frequency = 440 + (value * 2) self.createData() def play(self): if self.output.state() == QAudio.ActiveState: self.output.stop() if self.buffer.isOpen(): self.buffer.close() if self.output.error() == QAudio.UnderrunError: self.output.reset() self.buffer.setData(self.data) self.buffer.open(QIODevice.ReadOnly) self.buffer.seek(0) self.output.start(self.buffer) def changeVolume(self, value): self.volume = value self.createData() def createData(self): self.data.clear() for i in range(2 * 22050): t = i / 22050.0 value = int(self.volume * sin(2 * pi * self.frequency * t)) self.data.append(struct.pack("<h", value))