テラByteの時代にキロByte

shader又はdemosceneに関係する事

shaderシンセ用のクラスを作る

やはり、毎回コピペで作るのは、いかがなもので、クラスを作りました。
しかし、世間でいうライブラリーとは言い難い。自分が使いやすいように作りました。
受け入れられるかは、置いておいて載せてみます。 とりあえず、ファイル名は glSynth.py と言う事にしてください。これは単体でも動きます。
次回から、これを使った奴を書きます。
compute shaderで書いた奴とtransform feedbackで書いた奴を書きました。環境によっては使えない可能性が、あるので2つ書きました。
どちらかを glSynth.py として使ってください。
<追記>
compute shaderの場合GPUによりけりの仕様と思われるが、1回の処理をvec2又はvec4の単位でするものが、あるようだ。なので、dummyのvec2を置いて対応しました。とりあえず、不具合はなさそうです。でも、tansform feedbackを使ったclassの方が無難な気はします。

transform feedbackで書いた奴

from PySide2.QtWidgets import (QApplication, QWidget, QHBoxLayout, QVBoxLayout,
                             QPushButton, QDial, QLabel, 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)

# +++++++++++++++++++++++++++++++++++++

class GLSynth(QHLayoutWidget):
    def __init__(self, parent=None):
        super().__init__()
        self.sampleRate = 44100
        self.channelCount = 2
        self.bufferSize = 1024
        self.phase = 0
        self.shaderHead='''
#version 330
uniform float bufferSize;
uniform float sampleRate; 
uniform float phase; 
uniform float gate; 
out vec2 gain;
#define iTime ((bufferSize * phase + float(gl_VertexID)) / sampleRate)
#define iGateTime (bufferSize * gate / sampleRate)
'''       
        self.initializeWindow()
        self.initializeAudio()
        self.initializeGL()
        self.timer = QTimer(self)
        self.timer.timeout.connect(self.update)
        self.timer.start(10)
        
    def shaderScript(self):
        return '''
void main(){
    gain = vec2( sin(440.0 * 6.2831 * iTime) * step(iTime, iGateTime) );
}
'''

    def setUniform(self):
        pass

    def initializeWindow(self):
        pass
            
    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.audioOutput = QAudioOutput(format, self)
        self.stream = self.audioOutput.start()

    def initializeGL(self): 
        self.gl = QGLWidget()
        self.gl.makeCurrent()
        self.phase = 10000
        self.gate = 0
        self.program = glCreateProgram()
        shader = glCreateShader(GL_VERTEX_SHADER)
        glShaderSource(shader, self.shaderHead+self.shaderScript())
        glCompileShader(shader)
        if glGetShaderiv(shader, GL_COMPILE_STATUS) != GL_TRUE:
            self.error = glGetShaderInfoLog(shader).decode()
        glAttachShader(self.program, shader)
        outs = ctypes.cast(
            (ctypes.c_char_p*1)("gain".encode('utf-8')),
            ctypes.POINTER(ctypes.POINTER(ctypes.c_char)))
        glTransformFeedbackVaryings(self.program, 1, outs, GL_INTERLEAVED_ATTRIBS)
        glLinkProgram(self.program)
        glUseProgram(self.program)
        glUniform1f(glGetUniformLocation(self.program, "sampleRate"), self.sampleRate)
        glUniform1f(glGetUniformLocation(self.program, "bufferSize"), self.bufferSize)
        glUniform1f(glGetUniformLocation(self.program, "phase"), self.phase)
        glUniform1f(glGetUniformLocation(self.program, "gate"), self.gate)
        self.samples = (ctypes.c_float*self.bufferSize*self.channelCount)()
        vbo = glGenBuffers(1)
        glBindBuffer(GL_ARRAY_BUFFER, vbo)
        glBufferData(GL_ARRAY_BUFFER, sizeof(self.samples), None, GL_STATIC_DRAW)
        glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, vbo)
            
    def update(self):
        if self.audioOutput.bytesFree() > 0:
            glUseProgram(self.program)
            glUniform1f(glGetUniformLocation(self.program, "phase"), self.phase)
            glUniform1f(glGetUniformLocation(self.program, "gate"), self.gate)
            self.setUniform()
            self.phase += 1
            glEnable(GL_RASTERIZER_DISCARD)
            glBeginTransformFeedback(GL_POINTS)
            glDrawArrays(GL_POINTS, 0, self.bufferSize)
            glEndTransformFeedback()
            glDisable(GL_RASTERIZER_DISCARD)
            glGetBufferSubData(GL_ARRAY_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 setVolume(self, v):
        self.audioOutput.setVolume(v)

    def keyPressEvent(self, e):
        if e.isAutoRepeat(): return
        if e.key() == Qt.Key_Escape: self.close()
        self.phase = 0
        self.gate = 1<<32
        
    def keyReleaseEvent(self, e):
        if e.isAutoRepeat(): return
        self.gate = self.phase

if __name__ == '__main__':
    import sys
    app = QApplication(sys.argv)
    main_window = GLSynth()
    main_window.show()
    sys.exit(app.exec_())

compute shaderで書いた奴

from PySide2.QtWidgets import (QApplication, QWidget, QHBoxLayout, QVBoxLayout,
                             QPushButton, QDial, QLabel, 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)

# +++++++++++++++++++++++++++++++++++++

class GLSynth(QHLayoutWidget):
    def __init__(self, parent=None):
        super().__init__()
        self.sampleRate = 44100
        self.channelCount = 2
        self.bufferSize = 1024
        self.phase = 0
        self.shaderHead='''
#version 430
struct Sound{
    vec2 buf;
    vec2 dummy;
};

layout(std430, binding=7) buffer particles{
    Sound ins[];
}sound;

layout(local_size_x = 128, local_size_y = 1, local_size_z = 1) in;
uniform float bufferSize;
uniform float sampleRate; 
uniform float phase; 
uniform float gate; 
#define gain sound.ins[gl_GlobalInvocationID.x].buf
#define iTime ((bufferSize * phase + float(gl_GlobalInvocationID.x)) / sampleRate)
#define iGateTime (bufferSize * gate / sampleRate)
'''

        self.initializeWindow()
        self.initializeAudio()
        self.initializeGL()
        self.timer = QTimer(self)
        self.timer.timeout.connect(self.update)
        self.timer.start(10)
        
    def shaderScript(self):
        return '''
void main(){
    gain = vec2( sin(440.0 * 6.2831 * iTime) * step(iTime, iGateTime) );
}
'''

    def setUniform(self):
        pass

    def initializeWindow(self):
        pass
            
    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.audioOutput = QAudioOutput(format, self)
        self.stream = self.audioOutput.start()

    def initializeGL(self): 
        self.gl = QGLWidget()
        self.gl.makeCurrent()
        self.phase = 10000
        self.gate = 0
        self.program = glCreateProgram()    
        shader = glCreateShader(GL_COMPUTE_SHADER)
        glShaderSource(shader, self.shaderHead+self.shaderScript())
        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, "sampleRate"), self.sampleRate)
        glUniform1f(glGetUniformLocation(self.program, "bufferSize"), self.bufferSize)
        glUniform1f(glGetUniformLocation(self.program, "phase"), self.phase)
        glUniform1f(glGetUniformLocation(self.program, "gate"), self.gate)
        self.samples = (ctypes.c_float*self.bufferSize*4)()
        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, 7, vbo)
            
    def update(self):
        if self.audioOutput.bytesFree() > 0:
            glUseProgram(self.program)
            glUniform1f(glGetUniformLocation(self.program, "phase"), self.phase)
            glUniform1f(glGetUniformLocation(self.program, "gate"), self.gate)
            self.setUniform()
            self.phase += 1
            glDispatchCompute(self.bufferSize // 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)
            d = d.reshape((-1,2))[::2]
            d= d.flatten()
            self.stream.write(array.array('h', d*32767).tobytes())
    
    def setVolume(self, v):
        self.audioOutput.setVolume(v)

    def keyPressEvent(self, e):
        if e.isAutoRepeat(): return
        if e.key() == Qt.Key_Escape: self.close()
        self.phase = 0
        self.gate = 1<<32
        
    def keyReleaseEvent(self, e):
        if e.isAutoRepeat(): return
        self.gate = self.phase

if __name__ == '__main__':
    import sys
    app = QApplication(sys.argv)
    main_window = GLSynth()
    main_window.show()
    sys.exit(app.exec_())