class MainWindow(QMainWindow): def __init__(self, wav_path): QMainWindow.__init__(self) self.resize(350, 250) self.setWindowTitle('MainWindow') self._setLayout() self.status_bar = self.statusBar() self.wav_path = wav_path self.params = utils.read_wav_info(wav_path) self.duration = self.params.nframes / self.params.framerate self.output = utils.get_audio_output(self.params) self.output.stateChanged.connect(self.state_checkpoint) self.output.setNotifyInterval(20) self.output.notify.connect(self.notified) self.loop_button.clicked.connect(self.switch_loop) self.play_button.clicked.connect(self.play_pause) self.random_button.clicked.connect(self.set_random_region) self.export_button.clicked.connect(self.export_region) self.command_edit.returnPressed.connect(self.command_entered) self.loop_enabled = False self.buffer = QBuffer() self.region = None self.set_region((0, REG_SECONDS * self.params.framerate)) # self.set_random_region() def _setLayout(self): widget = QtWidgets.QWidget() grid = QtWidgets.QGridLayout(widget) self.progressBar = QtWidgets.QProgressBar(widget) self.progressBar.setRange(0, 100) self.progressBar.setValue(0) self.progressBar.setTextVisible(True) self.loop_button = QtWidgets.QPushButton('Loop', widget) self.loop_button.setCheckable(True) self.play_button = QtWidgets.QPushButton('Play | Stop', widget) self.random_button = QtWidgets.QPushButton('Random', widget) self.command_edit = QtWidgets.QLineEdit('') self.export_button = QtWidgets.QPushButton('Export', widget) grid.addWidget(self.progressBar, 0, 0, 1, 3) grid.addWidget(self.loop_button, 1, 0) grid.addWidget(self.play_button, 1, 1) grid.addWidget(self.random_button, 1, 2) grid.addWidget(self.command_edit, 2, 1) grid.addWidget(self.export_button, 2, 2) widget.setLayout(grid) self.setCentralWidget(widget) def play(self): """ Play from the beginning. """ if self.buffer.isOpen(): state = self.output.state() if state != QAudio.StoppedState: self.output.stop() if sys.platform == 'darwin': self.buffer.close() self.buffer.open(QIODevice.ReadOnly) else: # I found this way does not works on OS X self.buffer.seek(0) else: # Load from file self.buffer.open(QIODevice.ReadOnly) self.output.start(self.buffer) def play_pause(self): """ Play or pause based on audio output state. """ state = self.output.state() if state == QAudio.ActiveState: # playing # pause playback self.output.suspend() elif state == QAudio.SuspendedState: # paused # resume playback self.output.resume() elif state == QAudio.StoppedState or state == QAudio.IdleState: self.play() def stop(self): """ Stop playback. """ state = self.output.state() if state != QAudio.StoppedState: self.output.stop() if sys.platform == 'darwin': self.buffer.close() def switch_loop(self): self.loop_enabled = not self.loop_enabled def state_checkpoint(self): """ React to AudioOutput state change. Loop if enabled. """ # Loop implementation state = self.output.state() if state == QAudio.ActiveState: print(state, '== Active') elif state == QAudio.SuspendedState: print(state, '== Suspended') elif state == QAudio.IdleState: print(state, '== Idle') if self.loop_enabled: self.play() else: self.stop() elif state == QAudio.StoppedState: print(state, '== Stopped') def notified(self): start_time = self.region[0] / self.params.framerate playing_time = self.output.processedUSecs() / 1000000 + start_time self.progressBar.setValue(playing_time * 100 / self.duration) self.status_bar.showMessage(str(timedelta(seconds=playing_time))[:-3]) def set_region(self, region): """ Put the playback start position to `position`. """ # avoid segfault if changing region during playback self.stop() position, end = region position = max(0, min(position, end)) # don't start before 0 end = min(self.params.nframes, end) # don't set end after days! self.region = position, end print('set_region -> {:,}-{:,}'.format(*self.region)) print('region times: {}-{} (duration={})'.format(*self.region_timedeltas())) frame_to_read = end - position wav = wave.open(self.wav_path) wav.setpos(position) # we need to reinit buffer since the region could be shorter than before self.buffer = QBuffer() self.buffer.writeData(wav.readframes(frame_to_read)) wav.close() start_time = position / self.params.framerate self.progressBar.setValue(start_time * 100 / self.duration) self.status_bar.showMessage(str(timedelta(seconds=start_time))[:-3]) @property def reg_nframes(self): return self.region[1] - self.region[0] def set_random_region(self): """ Choose a random position and set playback start from there. """ try: position = random.randrange(self.params.nframes - self.reg_nframes) except ValueError: print('Cannot move position randomly. Please shorten the region.') position = 0 end = position + self.reg_nframes print('Random region: {:.2f}-{:.2f}'.format( position / self.params.framerate, end / self.params.framerate) ) self.set_region((position, end)) def region_timedeltas(self): """Return start, end and duration timedeltas""" start, end = self.region start_timedelta = timedelta(seconds=start / self.params.framerate) end_timedelta = timedelta(seconds=end / self.params.framerate) return start_timedelta, end_timedelta, (end_timedelta - start_timedelta) def command_entered(self): """ Change region boundaries with Blender-like syntax. Examples: "l-0.5" ==> move start position 0.5 s before "r1" ==> move stop position 1 seconds after """ command = self.command_edit.text() try: lr, delta = utils.parse_command(command) except (IndexError, ValueError) as err: print(err) return start, end = self.region if lr == 'l': start = int(start + delta * self.params.framerate) print('New start: {}'.format(timedelta(seconds=(start / self.params.framerate)))) elif lr == 'r': end = int(end + delta * self.params.framerate) print('New end: {}'.format(timedelta(seconds=(end / self.params.framerate)))) self.set_region((start, end)) self.command_edit.setText('') # feature: restart immediately after command is entered self.play() def export_region(self): """ Export the current region. """ start, stop = self.region wav_filepath = self.wav_path[:-4] + '[{}-{}].wav'.format(start, stop) with wave.open(wav_filepath, 'wb') as wave_write: wave_write.setparams(self.params) wave_write.writeframes(self.buffer.data()) print(wav_filepath, 'created')
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 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 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))