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 AudioWidget(QWidget): def __init__(self, parent=None): QWidget.__init__(self, parent) self.format = None self.output = None self.buffer = QBuffer() self.volumeSlider = QSlider(Qt.Horizontal) self.volumeSlider.setMaximum(10) self.volumeSlider.setPageStep(1) self.volumeSlider.setValue(5) self.playButton = QPushButton() self.playButton.setIcon(QIcon("icons/play.png")) self.stopButton = QPushButton() self.stopButton.setIcon(QIcon("icons/stop.png")) self.volumeSlider.valueChanged.connect(self.change_volume) self.playButton.clicked.connect(self.play_pause) self.stopButton.clicked.connect(self.stop) layout = QHBoxLayout(self) layout.addWidget(self.playButton) layout.addWidget(self.stopButton) layout.addWidget(self.volumeSlider) layout.addStretch() def stop(self): if self.output: if self.output.state() != QAudio.StoppedState: self.output.stop() def set_data(self, mono_sig, sr): # if not self.format: self.format = QAudioFormat() self.format.setChannelCount(1) self.format.setSampleRate(sr) #numpy is in bites, qt in bits self.format.setSampleSize(mono_sig.dtype.itemsize * 8) self.format.setCodec("audio/pcm") self.format.setByteOrder(QAudioFormat.LittleEndian) self.format.setSampleType(QAudioFormat.Float) self.output = QAudioOutput(self.format, self) self.output.stateChanged.connect(self.audio_state_changed) #change the content without stopping playback p = self.buffer.pos() if self.buffer.isOpen(): self.buffer.close() self.data = mono_sig.tobytes() self.buffer.setData(self.data) self.buffer.open(QIODevice.ReadWrite) self.buffer.seek(p) def audio_state_changed(self, new_state): #adjust the button icon if new_state != QAudio.ActiveState: self.playButton.setIcon(QIcon("icons/play.png")) else: self.playButton.setIcon(QIcon("icons/pause.png")) def cursor(self, t): #seek towards the time t #todo: handle EOF case try: if self.format: t = max(0, t) b = self.format.bytesForDuration(t * 1000000) self.buffer.seek(b) except: print("cursor error") def play_pause(self): if self.output: #(un)pause the audio output, keeps the buffer intact if self.output.state() == QAudio.ActiveState: self.output.suspend() elif self.output.state() == QAudio.SuspendedState: self.output.resume() else: self.buffer.seek(0) self.output.start(self.buffer) def change_volume(self, value): if self.output: #need to wrap this because slider gives not float output self.output.setVolume(value / 10)
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))