class ControllableAudio(QAudioOutput): # This links all the PyQt5 audio playback things - # QAudioOutput, QFile, and input from main interfaces def __init__(self, format): super(ControllableAudio, self).__init__(format) # on this notify, move slider (connected in main file) self.setNotifyInterval(30) self.stateChanged.connect(self.endListener) self.tempin = QBuffer() self.startpos = 0 self.timeoffset = 0 self.keepSlider = False #self.format = format # set small buffer (10 ms) and use processed time self.setBufferSize( int(self.format().sampleSize() * self.format().sampleRate() / 100 * self.format().channelCount())) def isPlaying(self): return (self.state() == QAudio.ActiveState) def endListener(self): # this should only be called if there's some misalignment between GUI and Audio if self.state() == QAudio.IdleState: # give some time for GUI to catch up and stop sleepCycles = 0 while (self.state() != QAudio.StoppedState and sleepCycles < 30): sleep(0.03) sleepCycles += 1 # This loop stops when timeoffset+processedtime > designated stop position. # By adding this offset, we ensure the loop stops even if # processed audio timer breaks somehow. self.timeoffset += 30 self.notify.emit() self.pressedStop() def pressedPlay(self, resetPause=False, start=0, stop=0, audiodata=None): if not resetPause and self.state() == QAudio.SuspendedState: print("Resuming at: %d" % self.pauseoffset) self.sttime = time.time() - self.pauseoffset / 1000 self.resume() else: if not self.keepSlider or resetPause: self.pressedStop() print("Starting at: %d" % self.tempin.pos()) sleep(0.2) # in case bar was moved under pause, we need this: pos = self.tempin.pos() # bytes pos = self.format().durationForBytes(pos) / 1000 # convert to ms pos = pos + start print("Pos: %d start: %d stop %d" % (pos, start, stop)) self.filterSeg(pos, stop, audiodata) def pressedPause(self): self.keepSlider = True # a flag to avoid jumping the slider back to 0 pos = self.tempin.pos() # bytes pos = self.format().durationForBytes(pos) / 1000 # convert to ms # store offset, relative to the start of played segment self.pauseoffset = pos + self.timeoffset self.suspend() def pressedStop(self): # stop and reset to window/segment start self.keepSlider = False self.stop() if self.tempin.isOpen(): self.tempin.close() def filterBand(self, start, stop, low, high, audiodata, sp): # takes start-end in ms, relative to file start self.timeoffset = max(0, start) start = max(0, start * self.format().sampleRate() // 1000) stop = min(stop * self.format().sampleRate() // 1000, len(audiodata)) segment = audiodata[int(start):int(stop)] segment = sp.bandpassFilter(segment, sampleRate=None, start=low, end=high) # segment = self.sp.ButterworthBandpass(segment, self.sampleRate, bottom, top,order=5) self.loadArray(segment) def filterSeg(self, start, stop, audiodata): # takes start-end in ms self.timeoffset = max(0, start) start = max(0, int(start * self.format().sampleRate() // 1000)) stop = min(int(stop * self.format().sampleRate() // 1000), len(audiodata)) segment = audiodata[start:stop] self.loadArray(segment) def loadArray(self, audiodata): # loads an array from memory into an audio buffer if self.format().sampleSize() == 16: audiodata = audiodata.astype( 'int16') # 16 corresponds to sampwidth=2 elif self.format().sampleSize() == 32: audiodata = audiodata.astype('int32') elif self.format().sampleSize() == 24: audiodata = audiodata.astype('int32') print("Warning: 24-bit sample playback currently not supported") elif self.format().sampleSize() == 8: audiodata = audiodata.astype('uint8') else: print("ERROR: sampleSize %d not supported" % self.format().sampleSize()) return # double mono sound to get two channels - simplifies reading if self.format().channelCount() == 2: audiodata = np.column_stack((audiodata, audiodata)) # write filtered output to a BytesIO buffer self.tempout = io.BytesIO() # NOTE: scale=None rescales using data minimum/max. This can cause clipping. Use scale="none" if this causes weird playback sound issues. # in particular for 8bit samples, we need more scaling: if self.format().sampleSize() == 8: scale = (audiodata.min() / 2, audiodata.max() * 2) else: scale = None wavio.write(self.tempout, audiodata, self.format().sampleRate(), scale=scale, sampwidth=self.format().sampleSize() // 8) # copy BytesIO@write to QBuffer@read for playing self.temparr = QByteArray(self.tempout.getvalue()[44:]) # self.tempout.close() if self.tempin.isOpen(): self.tempin.close() self.tempin.setBuffer(self.temparr) self.tempin.open(QIODevice.ReadOnly) # actual timer is launched here, with time offset set asynchronously sleep(0.2) self.sttime = time.time() - self.timeoffset / 1000 self.start(self.tempin) def seekToMs(self, ms, start): print("Seeking to %d ms" % ms) # start is an offset for the current view start, as it is position 0 in extracted file self.reset() self.tempin.seek(self.format().bytesForDuration((ms - start) * 1000)) self.timeoffset = ms def applyVolSlider(self, value): # passes UI volume nonlinearly # value = QAudio.convertVolume(value / 100, QAudio.LogarithmicVolumeScale, QAudio.LinearVolumeScale) value = (math.exp(value / 50) - 1) / (math.exp(2) - 1) self.setVolume(value)
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 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)