Ejemplo n.º 1
0
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"))
Ejemplo n.º 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()
Ejemplo n.º 3
0
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"))
Ejemplo n.º 4
0
class SleaZynth(QMainWindow):

    autoSaveFile = 'auto.save'

    texsize = 512
    samplerate = 44100

    def __init__(self):
        QMainWindow.__init__(self)
        self.ui = Ui_MainWindow()
        self.ui.setupUi(self)
        self.show()

        self.initModelView()
        self.initSignals()

        self.initState()
        self.autoLoad()

        self.initAMaySyn()
        self.initAudio()

    def keyPressEvent(self, event):
        if event.key() == Qt.Key_Escape:
            self.close()

        elif event.key() == Qt.Key_F1:
            self.debugOutput()

        if event.modifiers() & Qt.ControlModifier:

            if event.key() == Qt.Key_S:
                self.autoSave()

            elif event.key() == Qt.Key_L:
                self.autoLoad()

            elif event.key() == Qt.Key_T:
                self.renderWhateverWasLast()

    def closeEvent(self, event):
        QApplication.quit()

    def initSignals(self):
        self.ui.btnChooseFilename.clicked.connect(self.loadAndImportMayson)
        self.ui.btnImport.clicked.connect(self.importMayson)
        self.ui.btnExport.clicked.connect(self.exportChangedMayson)
        self.ui.checkAutoReimport.clicked.connect(self.toggleAutoReimport)
        self.ui.checkAutoRender.clicked.connect(self.toggleAutoRender)

        self.ui.editFilename.editingFinished.connect(
            partial(self.updateStateFromUI, only='maysonFile'))
        self.ui.editBPM.editingFinished.connect(
            partial(self.updateStateFromUI, only='BPM'))
        self.ui.spinBOffset.valueChanged.connect(
            partial(self.updateStateFromUI, only='B_offset'))
        self.ui.spinBStop.valueChanged.connect(
            partial(self.updateStateFromUI, only='B_stop'))
        self.ui.spinLevelSyn.valueChanged.connect(
            partial(self.updateStateFromUI, only='level_syn'))
        self.ui.spinLevelDrum.valueChanged.connect(
            partial(self.updateStateFromUI, only='level_drum'))
        self.ui.checkWriteWAV.clicked.connect(
            partial(self.updateStateFromUI, only='writeWAV'))
        self.ui.spinTimeShift.valueChanged.connect(
            partial(self.updateStateFromUI, only='extraTimeShift'))

        self.ui.editTrackName.textChanged.connect(self.trackSetName)
        self.ui.spinTrackVolume.valueChanged.connect(self.trackSetVolume)
        self.ui.checkTrackMute.stateChanged.connect(self.trackSetMute)
        self.ui.spinModOn.valueChanged.connect(self.moduleSetModOn)
        self.ui.spinModTranspose.valueChanged.connect(self.moduleSetTranspose)
        self.ui.btnApplyPattern.clicked.connect(self.moduleSetPattern)
        self.ui.btnApplyNote.clicked.connect(self.noteApplyChanges)
        self.ui.btnTrackClone.clicked.connect(self.trackClone)
        self.ui.btnTrackDelete.clicked.connect(self.trackDelete)
        self.ui.btnRandomSynth.clicked.connect(self.trackSetRandomSynth)
        self.ui.btnRandomizeSynth.clicked.connect(self.synthRandomize)
        self.ui.btnSaveSynth.clicked.connect(self.synthHardClone)
        self.ui.btnApplySynthName.clicked.connect(self.synthChangeName)
        self.ui.btnReloadSyn.clicked.connect(self.loadSynthsFromSynFile)

        self.ui.btnRenderModule.clicked.connect(self.renderModule)
        self.ui.btnRenderTrack.clicked.connect(self.renderTrack)
        self.ui.btnRenderSong.clicked.connect(self.renderSong)
        self.ui.btnStopPlayback.clicked.connect(self.stopPlayback)

        # model/view signals
        self.ui.trackList.selectionModel().currentChanged.connect(
            self.trackLoad)
        self.ui.patternCombox.currentIndexChanged.connect(self.patternLoad)
        self.ui.moduleList.selectionModel().currentChanged.connect(
            self.moduleLoad)
        self.ui.synthList.selectionModel().currentChanged.connect(
            self.trackSetSynth)
        self.ui.noteList.selectionModel().currentChanged.connect(self.noteLoad)
        self.ui.drumList.selectionModel().currentChanged.connect(
            self.noteSetDrum)

    def initModelView(self):
        self.trackModel = TrackModel()
        self.ui.trackList.setModel(self.trackModel)
        self.moduleModel = ModuleModel()
        self.ui.moduleList.setModel(self.moduleModel)
        self.patternModel = PatternModel()
        self.ui.patternCombox.setModel(self.patternModel)
        self.noteModel = NoteModel()
        self.ui.noteList.setModel(self.noteModel)
        self.synthModel = QStringListModel()
        self.ui.synthList.setModel(self.synthModel)
        self.drumModel = QStringListModel()
        self.ui.drumList.setModel(self.drumModel)

        self.noteModel.dataChanged.connect(
            self.updateModulesWithChangedPattern)
        self.noteModel.reloadNoteParameters.connect(self.noteLoad)

    def initState(self):
        self.state = {
            'maysonFile': '',
            'autoReimport': False,
            'autoRender': False,
            'lastRendered': '',
            'writeWAV': False,
            'selectedTrack': 0,
            'selectedModule': 0,
            'extraTimeShift': 0,
        }
        self.info = {}
        self.patterns = []
        self.synths = []
        self.drumkit = []
        self.amaysyn = None
        self.fileObserver = None

    def loadAndImportMayson(self):
        name, _ = QFileDialog.getOpenFileName(
            self, 'Load MAYSON file', '', 'aMaySyn export *.mayson(*.mayson)')
        if name == '':
            return
        self.state['maysonFile'] = name
        self.state['title'], self.state[
            'synFile'] = self.getTitleAndSynFromMayson(name)
        self.autoSave()
        self.importMayson()

    def importMayson(self):
        maysonData = {}
        try:
            file = open(self.state['maysonFile'], 'r')
            maysonData = json.load(file)
        except FileNotFoundError:
            print(
                f"{self.state['maysonFile']} could not be imported. make sure that it exists, or choose another one."
            )
            self.loadAndImportMayson()
        except json.decoder.JSONDecodeError:
            print(
                f"{self.state['maysonFile']} is changing right now, pause for 1 sec..."
            )
            sleep(1)
            self.importMayson()
        finally:
            file.close()

        if maysonData == {}:
            return

        self.info = maysonData['info']
        self.info.update({'title': self.state['title']})
        if self.amaysyn is not None:
            self.amaysyn.updateState(info=self.info)

        self.trackModel.setTracks(maysonData['tracks'])
        self.patternModel.setPatterns(maysonData['patterns'])
        self.synthModel.setStringList(maysonData['synths'])
        self.drumModel.setStringList(maysonData['drumkit'])

        self.trackModel.layoutChanged.emit()
        if self.state['selectedTrack'] >= self.trackModel.rowCount():
            self.state['selectedTrack'] = 0
        self.selectIndex(self.ui.trackList, self.trackModel,
                         self.state['selectedTrack'])

        if self.state['selectedModule'] >= self.moduleModel.rowCount():
            self.state['selectedModule'] = 0
        self.selectIndex(self.ui.moduleList, self.moduleModel,
                         self.state['selectedModule'])

        self.noteModel.layoutChanged.emit()
        if self.noteModel.rowCount() > 0:
            self.selectIndex(self.ui.noteList, self.noteModel, 0)

        self.synthModel.layoutChanged.emit()

        self.drumModel.layoutChanged.emit()
        if self.drumIndex().isValid():
            self.selectIndex(self.ui.drumList, self.drumModel,
                             self.drumIndex().row())

        self.applyStateToUI()

    def exportChangedMayson(self):
        name, _ = QFileDialog.getSaveFileName(
            self, 'Export with Changes', self.state['maysonFile'],
            'aMaySyn export *.mayson(*.mayson)')
        if name == '':
            return
        data = {
            'info': self.info,
            'tracks': self.trackModel.tracks,
            'patterns': self.patternModel.patterns,
            'synths': self.synthModel.stringList(),
            'drumkit': self.drumModel.stringList(),
        }
        file = open(name, 'w')
        json.dump(data, file)
        file.close()

    def updateStateFromUI(self, only=None):
        if only is None or only == 'maysonFile':
            self.state.update({'maysonFile': self.ui.editFilename.text()})
        title, synFile = self.getTitleAndSynFromMayson(
            self.state['maysonFile'])
        self.state.update({'synFile': synFile})
        self.state.update({'title': title})
        self.info['title'] = title
        if only is None or only == 'BPM':
            self.info['BPM'] = self.ui.editBPM.text()
        if only is None or only == 'B_offset':
            self.info['B_offset'] = self.ui.spinBOffset.value()
        if only is None or only == 'B_stop':
            self.info['B_stop'] = self.ui.spinBStop.value()
        if only is None or only == 'level_syn':
            self.info['level_syn'] == self.ui.spinLevelSyn.value()
        if only is None or only == 'level_drum':
            self.info['level_drum'] == self.ui.spinLevelDrum.value()
        if only is None or only == 'writeWAV':
            self.state['writeWAV'] = self.ui.checkWriteWAV.isChecked()
        if only is None or only == 'extraTimeShift':
            self.state['extraTimeShift'] = self.ui.spinTimeShift.value()

        if self.amaysyn is not None:
            self.amaysyn.updateState(info=self.info, synFile=synFile)

    def applyStateToUI(self):
        self.ui.editFilename.setText(self.state['maysonFile'])
        # TODO: think about - do I want self.state['override']['BPM'] etc.??
        self.ui.editBPM.setText(self.info['BPM'])
        self.ui.spinBOffset.setValue(self.info['B_offset'])
        self.ui.spinBStop.setValue(self.info['B_stop'])
        self.ui.spinLevelSyn.setValue(self.info['level_syn'])
        self.ui.spinLevelDrum.setValue(self.info['level_drum'])
        self.ui.checkAutoReimport.setChecked(self.state['autoReimport'])
        self.ui.checkAutoRender.setChecked(self.state['autoRender'])
        self.ui.checkWriteWAV.setChecked(self.state['writeWAV'])
        self.ui.spinTimeShift.setValue(self.state['extraTimeShift'])

    def autoSave(self):
        file = open(self.autoSaveFile, 'w')
        json.dump(self.state, file)
        file.close()

    def autoLoad(self):
        loadState = {}
        try:
            file = open(self.autoSaveFile, 'r')
            loadState = json.load(file)
            file.close()
        except FileNotFoundError:
            pass

        for key in loadState:
            self.state[key] = loadState[key]

        if 'autoReimport' in self.state:
            self.toggleAutoReimport(self.state['autoReimport'])
        if 'maysonFile' not in self.state or self.state['maysonFile'] == '':
            self.loadAndImportMayson()
        else:
            self.importMayson()

    def toggleAutoRender(self, checked):
        self.state['autoRender'] = checked
        self.autoSave()

    def toggleAutoReimport(self, checked):
        self.state['autoReimport'] = checked
        self.autoSave()

        if self.fileObserver is not None:
            self.fileObserver.stop()
            self.fileObserver.join()
            self.fileObserver = None

        if checked:
            file = self.state['maysonFile']
            eventHandler = FileModifiedHandler(file)
            eventHandler.fileChanged.connect(
                self.importAndRender if self.state['autoRender'] else self.
                importMayson)
            self.fileObserver = Observer()
            self.fileObserver.schedule(eventHandler,
                                       path=path.dirname(file),
                                       recursive=False)
            self.fileObserver.start()

    def importAndRender(self):
        self.importMayson()
        if self.amaysyn is None:
            print(
                "You want to Reimport&Render, but why is aMaySyn not initialized? do some rendering first!"
            )
            return
        self.renderWhateverWasLast()

#################################### GENERAL HELPERS ###########################################

    def selectIndex(self, list, model, index):
        list.selectionModel().setCurrentIndex(
            model.createIndex(index, 0), QItemSelectionModel.SelectCurrent)

    def patternIndexOfName(self, name):
        patternNames = [p['name'] for p in self.patternModel.patterns]
        if name in patternNames:
            return patternNames.index(name)
        else:
            return None

    def getTitleAndSynFromMayson(self, maysonFile):
        synFile = '.'.join(maysonFile.split('.')[:-1]) + '.syn'
        title = '.'.join(path.basename(maysonFile).split('.')[:-1])
        return title, synFile

    def placeholder(self):
        print("FUNCTION NOT IMPLEMENTED. Sorrriiiiiiieee! (not sorry.)")

#################################### TRACK FUNCTIONALITY #######################################

    def track(self):
        return self.trackModel.tracks[self.trackIndex().row(
        )] if self.trackModel.rowCount() > 0 else None

    def trackIndex(self):
        return self.ui.trackList.currentIndex()

    def trackModelChanged(self):
        self.trackModel.dataChanged.emit(self.trackIndex(), self.trackIndex())

    def trackLoad(self, currentIndex):
        cTrack = self.trackModel.tracks[currentIndex.row()]
        self.ui.editTrackName.setText(cTrack['name'])
        self.ui.spinTrackVolume.setValue(100 * cTrack['par_norm'])
        self.ui.checkTrackMute.setChecked(not cTrack['mute'])
        self.moduleModel.setModules(cTrack['modules'])
        if len(cTrack['modules']) > 0:
            self.selectIndex(self.ui.moduleList, self.moduleModel,
                             cTrack['current_module'])
            self.moduleLoad()
        self.selectIndex(self.ui.synthList, self.synthModel,
                         cTrack['current_synth'])
        self.state['selectedTrack'] = currentIndex.row()

    def trackClone(self):
        self.trackModel.cloneRow(self.trackIndex().row())

    def trackDelete(self):
        self.trackModel.removeRow(self.trackIndex().row())

    def trackSetName(self, name):
        self.track()['name'] = name
        self.trackModelChanged()

    def trackSetVolume(self, value):
        self.track()['par_norm'] = round(value * .01, 3)
        self.trackModelChanged()

    def trackSetMute(self, state):
        self.track()['mute'] = (state != Qt.Checked)
        self.trackModelChanged()

    def trackSetSynth(self, index):
        self.track()['current_synth'] = self.synthModel.stringList().index(
            self.synthModel.data(index, Qt.DisplayRole))
        self.ui.editSynthName.setText(self.synthName())
        self.trackModelChanged()

        if self.synth()[0] == 'D':
            self.noteModel.useDrumkit(self.drumModel.stringList())
        else:
            self.noteModel.useDrumkit(None)

    def trackSetRandomSynth(self):
        randomIndex = self.synthModel.createIndex(
            randint(0,
                    len(self.instrumentSynths()) - 1), 0)
        self.ui.synthList.setCurrentIndex(randomIndex)

#################################### MODULE FUNCTIONALITY ######################################

    def module(self):
        return self.moduleModel.modules[self.moduleIndex().row(
        )] if self.moduleModel.rowCount() > 0 else None

    def moduleIndex(self):
        return self.ui.moduleList.currentIndex()

    def moduleModelChanged(self):
        self.moduleModel.dataChanged.emit(self.moduleIndex(),
                                          self.moduleIndex())

    def moduleLoad(self, currentIndex=None):
        if currentIndex is None:
            cModule = self.module()
        else:
            cModule = self.moduleModel.modules[currentIndex.row()]
            self.state['selectedModule'] = currentIndex.row()
        self.ui.patternCombox.setCurrentIndex(
            self.patternIndexOfName(cModule['pattern']['name']))
        self.ui.spinModOn.setValue(cModule['mod_on'])
        self.ui.spinModTranspose.setValue(cModule['transpose'])

    def moduleAssignPattern(self, pattern):
        self.module()['pattern'] = pattern  # deepcopy(pattern)

    def moduleSetPattern(self):
        self.moduleAssignPattern(self.pattern())
        self.moduleModelChanged()

    def moduleSetModOn(self, value):
        self.module()['mod_on'] = self.ui.spinModOn.value()
        self.moduleModelChanged()

    def moduleSetTranspose(self, value):
        self.module()['transpose'] = self.ui.spinModTranspose.value()
        self.moduleModelChanged()

#################################### PATTERN FUNCTIONALITY #####################################

    def pattern(self):
        return self.patternModel.patterns[
            self.patternIndex()] if self.patternModel.rowCount() > 0 else None

    def patternIndex(self):
        return self.ui.patternCombox.currentIndex()

    def patternLoad(self, currentIndex):
        cPattern = self.patternModel.patterns[currentIndex]
        self.noteModel.setNotes(cPattern['notes'])

    def updateModulesWithChangedPattern(self, rowBegin, rowEnd):
        self.moduleAssignPattern(self.pattern())
        self.moduleModelChanged()
        self.trackModel.updateModulesWithChangedPattern(self.pattern())
        self.trackModelChanged()

#################################### NOTE FUNCTIONALITY ########################################

    def note(self):
        return self.noteModel.notes[
            self.noteIndex().row()] if self.noteModel.rowCount() > 0 else None

    def noteIndex(self):
        return self.ui.noteList.currentIndex()

    def noteModelChanged(self):
        self.noteModel.dataChanged.emit(self.noteIndex(), self.noteIndex())

    def noteLoad(self, currentIndex):
        self.ui.editNote.setText(
            self.noteModel.data(currentIndex, Qt.DisplayRole))
        self.ui.editNote.setCursorPosition(0)

    def noteApplyChanges(self):
        self.noteModel.changeByString(self.noteIndex(),
                                      self.ui.editNote.text())

    def noteSetDrum(self, currentIndex):
        if self.drum() is not None:
            self.noteModel.changeDrumTo(self.noteIndex(), self.drum())

################################ SYNTH / DRUM FUNCTIONALITY ####################################

    def synth(self):
        return self.synthModel.data(self.ui.synthList.currentIndex(),
                                    Qt.DisplayRole)

    def synthName(self):
        return self.synth()[2:]

    def instrumentSynths(self):
        return [
            I_synth for I_synth in self.synthModel.stringList()
            if I_synth[0] == 'I'
        ]

    def drum(self):
        if not self.drumIndex():
            print("LOLOLOL DRUM INDEX IS NONE (should never happen)")
        if not self.drumIndex().isValid():
            print("LOLOLOL DRUM INDEX NOT VALID")
        return self.drumModel.data(self.drumIndex(), Qt.DisplayRole)

    def drumIndex(self):
        return self.ui.drumList.currentIndex()

    def synthRandomize(self):
        self.amaysyn.aMaySynatize(reshuffle_randoms=True)

    def synthHardClone(self):
        if self.synth()[0] == 'D':
            self.synthHardCloneDrum(self)
            return
        else:
            count = 0
            oldID = self.synthName()
            synths = self.instrumentSynths()
            while True:
                formID = oldID + '.' + str(count)
                print("TRYING", formID, synths)
                if 'I_' + formID not in synths: break
                count += 1

            try:
                formTemplate = next(
                    form for form in self.amaysyn.last_synatized_forms
                    if form['id'] == oldID)
                formType = formTemplate['type']
                formMode = formTemplate['mode']
                formBody = ' '.join(key + '=' + formTemplate[key]
                                    for key in formTemplate
                                    if key not in ['type', 'id', 'mode'])
                if formMode: formBody += ' mode=' + ','.join(formMode)
            except StopIteration:
                print(
                    "Current synth is not compiled yet. Do so and try again.")
                return
            except:
                print("could not CLONE HARD:", formID, formTemplate)
                raise
            else:
                with open(self.state['synFile'], mode='a') as filehandle:
                    filehandle.write('\n' + formType + 4 * ' ' + formID +
                                     4 * ' ' + formBody)
                self.loadSynthsFromSynFile()

    def synthHardDrum(self):
        print("NOT IMPLEMENTED YET")
        return
        count = 0
        oldID = self.synthName()
        synths = self.instrumentSynths()
        while True:
            formID = oldID + '.' + str(count)
            print("TRYING", formID, synths)
            if 'I_' + formID not in synths: break
            count += 1

        try:
            formTemplate = next(form
                                for form in self.amaysyn.last_synatized_forms
                                if form['id'] == oldID)
            formType = formTemplate['type']
            formMode = formTemplate['mode']
            formBody = ' '.join(key + '=' + formTemplate[key]
                                for key in formTemplate
                                if key not in ['type', 'id', 'mode'])
            if formMode: formBody += ' mode=' + ','.join(formMode)
        except StopIteration:
            print("Current synth is not compiled yet. Do so and try again.")
            return
        except:
            print("could not CLONE HARD:", formID, formTemplate)
            raise
        else:
            with open(self.state['synFile'], mode='a') as filehandle:
                filehandle.write('\n' + formType + 4 * ' ' + formID + 4 * ' ' +
                                 formBody)
            self.loadSynthsFromSynFile()

    def synthChangeName(self):
        if self.synth()[0] != 'I':
            print("Nah. Select an instrument synth (I_blabloo)")
            return
        newID = self.ui.editSynthName.text()
        if newID == '':
            return
        formID = self.synthName()
        tmpFile = self.state['synFile'] + '.tmp'
        move(self.state['synFile'], tmpFile)
        with open(tmpFile, mode='r') as tmp_handle:
            with open(self.state['synFile'], mode='w') as new_handle:
                for line in tmp_handle.readlines():
                    lineparse = line.split()
                    if len(lineparse) > 2 and lineparse[0] in [
                            'main', 'maindrum'
                    ] and lineparse[1] == formID:
                        new_handle.write(
                            line.replace(' ' + formID + ' ',
                                         ' ' + newID + ' '))
                    else:
                        new_handle.write(line)
        self.loadSynthsFromSynFile()

    def loadSynthsFromSynFile(self):
        self.amaysyn.aMaySynatize()
        self.synthModel.setStringList(self.amaysyn.synths)
        self.synthModel.dataChanged.emit(
            self.synthModel.createIndex(0, 0),
            self.synthModel.createIndex(self.synthModel.rowCount(), 0))
        self.drumModel.setStringList(self.amaysyn.drumkit)
        self.drumModel.dataChanged.emit(
            self.drumModel.createIndex(0, 0),
            self.drumModel.createIndex(self.drumModel.rowCount(), 0))
        self.trackModel.setSynthList(self.amaysyn.synths)
        self.trackModelChanged()

# TODO: function to change drumkit order / assignment?

######################################## SleaZYNTHesizer #######################################

    def initAMaySyn(self):
        self.amaysyn = aMaySynBuilder(self, self.state['synFile'], self.info)

    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.audiooutput = QAudioOutput(self.audioformat)
        self.audiooutput.setVolume(1.0)

    def stopPlayback(self):
        self.audiooutput.stop()

    def renderWhateverWasLast(self):
        if self.state['lastRendered'] == 'module':
            self.renderModule()
        elif self.state['lastRendered'] == 'track':
            self.renderTrack()
        else:
            self.renderSong()

    def renderModule(self):
        print(self.track(), self.module())
        self.state['lastRendered'] = 'module'
        restoreMute = self.track()['mute']
        self.track()['mute'] = False
        modInfo = deepcopy(self.info)
        modInfo['B_offset'] = self.module()['mod_on']
        modInfo['B_stop'] = self.module()['mod_on'] + self.module(
        )['pattern']['length']
        self.amaysyn.info = modInfo
        self.amaysyn.extra_time_shift = self.state['extraTimeShift']
        shader = self.amaysyn.build(tracks=[self.track()],
                                    patterns=[self.module()['pattern']])
        self.amaysyn.info = self.info
        self.track()['mute'] = restoreMute
        self.executeShader(shader)

    def renderTrack(self):
        self.state['lastRendered'] = 'track'
        restoreMute = self.track()['mute']
        self.track()['mute'] = False
        self.amaysyn.extra_time_shift = self.state['extraTimeShift']
        shader = self.amaysyn.build(tracks=[self.track()],
                                    patterns=self.patternModel.patterns)
        self.track()['mute'] = restoreMute
        self.executeShader(shader)

    def renderSong(self):
        self.state['lastRendered'] = 'song'
        self.amaysyn.extra_time_shift = self.state['extraTimeShift']
        shader = self.amaysyn.build(tracks=self.trackModel.tracks,
                                    patterns=self.patternModel.patterns)
        self.executeShader(shader)

    def executeShader(self, shader):
        self.ui.codeEditor.clear()
        self.ui.codeEditor.insertPlainText(
            shader.replace(4 * ' ', '\t').replace(3 * ' ', '\t'))
        self.ui.codeEditor.ensureCursorVisible()

        sequenceLength = len(
            self.amaysyn.sequence) if self.amaysyn.sequence is not None else 0
        if not self.amaysyn.useSequenceTexture and sequenceLength > pow(2, 14):
            QMessageBox.critical(
                self, "I CAN'T",
                f"Either switch to using the Sequence Texture (ask QM), or reduce the sequence size by limiting the offset/stop positions or muting tracks.\nCurrent sequence length is:\n{sequenceLength} > {pow(2,14)}"
            )
            return

        self.bytearray = self.amaysyn.executeShader(
            shader,
            self.samplerate,
            self.texsize,
            renderWAV=self.state['writeWAV'])
        self.audiobuffer = QBuffer(self.bytearray)
        self.audiobuffer.open(QIODevice.ReadOnly)
        self.audiooutput.stop()
        self.audiooutput.start(self.audiobuffer)

###################################### DEBUG STUFF #############################################

    def debugOutput(self):
        print("TRACKS:", self.trackModel.rowCount())
        print("===== TRACK ACCUMULATION =====")
        track_accumulate = 0
        for t in self.trackModel.tracks:
            delta = len(t['modules'])
            print(
                f"{t['name']:>20} {track_accumulate:>10} {track_accumulate + delta:>10}"
            )
            track_accumulate += delta
        print("END AT", track_accumulate)
        print()
        print("PATTERNS:", self.patternModel.rowCount())
        print("===== PATTERN ACCUMULATION =====")
        pattern_accumulate = 0
        for p in self.patternModel.patterns:
            delta = len(p['notes'])
            print(
                f"{p['name']:>20} {pattern_accumulate:>10} {pattern_accumulate + delta:>10}"
            )
            pattern_accumulate += delta
        print("END AT", pattern_accumulate)
        print()
Ejemplo n.º 5
0
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
Ejemplo n.º 6
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))