예제 #1
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)
예제 #2
0
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()
예제 #3
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)