pySide2を使ってリアルタイムでmusic shaderを鳴らす
pyside2を使ってリアルタイムでmusic shaderを鳴らしてみました。
リアルタイムなので、時間の制限がありません。これができればシンセサイザーを作るのも可能になってきます。
やっと、始められます。
<追記>
このscriptは正常に動かないGPUがあります。どうやら一回の処理にvec2で処理するかvec4で処理するかによるものらしい。
これは、GeForce GTX 960では動いています。無難にtransform feedbackの方が良い気はします。
from PySide2.QtWidgets import (QApplication, QWidget, QHBoxLayout, QVBoxLayout, QPushButton, QStyle) from PySide2.QtCore import Qt, QTimer, QIODevice from PySide2.QtMultimedia import QAudioFormat, QAudioOutput from PySide2.QtOpenGL import QGLWidget from OpenGL.GL import * import ctypes import numpy import array # ++++++++++++++++++++++++++++++++++++ # https://qiita.com/gaziya5/items/e3cdb0251c01e76cbb05 class QHLayoutWidget(QWidget): def __init__(self): super().__init__() self.layout = QHBoxLayout() self.setLayout(self.layout) def addWidget(self,w): self.layout.addWidget(w) class QVLayoutWidget(QWidget): def __init__(self): super().__init__() self.layout = QVBoxLayout() self.setLayout(self.layout) def addWidget(self,w): self.layout.addWidget(w) # +++++++++++++++++++++++++++++++++++++ # music shader shadertoy = ''' #define BPM 140. #define A (15./BPM) float adsr(float t, vec4 e, float s) { return max(0.0, min(1.0, t/max(0.0001, e.x)) - min((1.0 - s) ,max(0.0, t - e.x)*(1.0 - s)/max(0.0001, e.y)) - max(0.0, t - e.z)*s/max(0.0001, e.w)); } float square(float f) { return sign(fract(f)-0.5); } float noise(float t) { return square(2763.*t*sin(t*8000.)); } float kick(float t){ return cos(315. * t - 10. * exp( -50. * t )+0.3)*adsr(t,vec4(0.0, 0.3, 0.0, 0.0), 0.0); +0.2*square(50.*t)* adsr(t,vec4(0.0, 0.05, 0.0, 0.0), 0.0); } // http://www.tsurishi.info/chiptune-neiro-edit-drum/ float snare(float t) { return square(3063.*t*sin(t*8000.)) * adsr(t,vec4(0.005, 0.08, 0.0, 0.0), 0.0); } // http://www.tsurishi.info/chiptune-neiro-edit-hihat/ float closeHihat(float t) { return square(2763.*t*sin(t*8500.)) * adsr(t,vec4(0.0, 0.03, 0.0, 0.0), 0.0); } float openHihat(float t) { return square(2763.*t*sin(t*8300.)) * adsr(t,vec4(0.0, 0.05, 0.03, 0.03), 0.5); } // https://qiita.com/gaziya5/items/e58f8c1fce3f3f227ca7 float sequence(int s,float t) { float n =mod(t,A); for(int i=0;i<16;i++){ if((s>>(int(t/A)-i)%16&1)==1)break; n+=A; } return n; } // http://www.spotlight-jp.com/matsutake/mt/images/ArmenBreakTab.jpg vec2 mainSound( float time ) { int i = int(floor(time/(A*16.)))&3; int velocity = int[](0x3030,0x3030,0x3030,0x3030)[i]>>(int(floor(time/A))&15)&1; float vol = 0.2 *(1.0+1.5*float(velocity)); return vec2( 0.0 +0.4 * kick(sequence( int[](0x0c05,0x0c05,0x0c05,0x0c0c)[i],time)) +0.3 * snare(sequence( int[](0x9290,0x9290,0x4290,0x4292)[i],time)) +vol * closeHihat(sequence(int[](0x5555,0x5555,0x5555,0x5155)[i],time)) +vol * openHihat(sequence( int[](0x0000,0x0000,0x0000,0x0400)[i],time)) ); } ''' script = ''' #version 430 layout(binding=1) buffer Buf{ vec2 buf[];}; layout(local_size_x = 128, local_size_y = 1, local_size_z = 1) in; uniform float phase; uniform float bufferSize; uniform float sampleRate; ''' + shadertoy + ''' void main(){ uint id = gl_GlobalInvocationID.x; float time = (bufferSize * phase + float(id)) / sampleRate; buf[id] = mainSound(time); } ''' class MainWindow(QHLayoutWidget): def __init__(self, parent=None): super().__init__() self.sampleRate = 44100 self.channelCount = 2 self.bufferSize = 1024 self.phase = 0 self.initializeWindow() self.initializeAudio() self.initializeGL() self.timer = QTimer(self) self.timer.timeout.connect(self.update) self.timer.start(10) def initializeWindow(self): audioLayout = QHLayoutWidget(); self.layout.addWidget(audioLayout) backwardButton = QPushButton(); audioLayout.addWidget(backwardButton) self.ctrlButton = QPushButton(); audioLayout.addWidget(self.ctrlButton) audioLayout.layout.setContentsMargins(0, 0, 0, 0) backwardButton.setIcon(self.style().standardIcon(QStyle.SP_MediaSkipBackward)) self.ctrlButton.setIcon(self.style().standardIcon(QStyle.SP_MediaPause)) backwardButton.clicked.connect(self.backwardButton_clicked) self.ctrlButton.clicked.connect(self.ctrlButton_clicked) def initializeAudio(self): format = QAudioFormat() format.setSampleRate(self.sampleRate) format.setChannelCount(self.channelCount) format.setSampleSize(16) format.setCodec("audio/pcm") format.setByteOrder(QAudioFormat.LittleEndian) format.setSampleType(QAudioFormat.SignedInt) self.audio = QAudioOutput(format, self) self.stream = self.audio.start() def initializeGL(self): self.gl = QGLWidget() self.gl.makeCurrent() self.variable = { "sampleRate": "sampleRate", "bufferSize": "bufferSize", "phase": "phase" } self.program = glCreateProgram() shader = glCreateShader(GL_COMPUTE_SHADER) glShaderSource(shader, script) glCompileShader(shader) #if glGetShaderiv(shader, GL_COMPILE_STATUS) != GL_TRUE: # raise RuntimeError(glGetShaderInfoLog(shader).decode()) glAttachShader(self.program, shader) glLinkProgram(self.program) glUseProgram(self.program) glUniform1f(glGetUniformLocation(self.program, self.variable["sampleRate"]), self.sampleRate) glUniform1f(glGetUniformLocation(self.program, self.variable["bufferSize"]), self.bufferSize) self.samples = (ctypes.c_float*self.bufferSize*self.channelCount)() vbo = glGenBuffers(1) glBindBuffer(GL_SHADER_STORAGE_BUFFER, vbo) glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(self.samples), None, GL_STATIC_DRAW) glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, vbo) def update(self): if self.audio.bytesFree() > 0: glUseProgram(self.program) glUniform1f(glGetUniformLocation(self.program, self.variable["phase"]), self.phase) self.phase += 1 glDispatchCompute(self.bufferSize*self.channelCount // 128, 1, 1) glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, sizeof(self.samples), ctypes.byref(self.samples)) d = numpy.frombuffer(self.samples, dtype=numpy.float32) d = numpy.maximum(d, -1) d = numpy.minimum(d, 1) self.stream.write(array.array('h', d*32767).tobytes()) def ctrlButton_clicked(self): if self.timer.isActive(): self.timer.stop() self.ctrlButton.setIcon(self.style().standardIcon(QStyle.SP_MediaPlay)) else: self.timer.start(10) self.ctrlButton.setIcon(self.style().standardIcon(QStyle.SP_MediaPause)) def backwardButton_clicked(self): self.phase = 0 def keyPressEvent(self, e): if e.key() == Qt.Key_Escape: self.close() if __name__ == '__main__': import sys app = QApplication(sys.argv) main_window = MainWindow() main_window.show() sys.exit(app.exec_()) '''