Exemple #1
0
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')
Exemple #2
0
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)
Exemple #3
0
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)
Exemple #4
0
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))