テラByteの時代にキロByte

shader又はdemosceneに関係する事

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_())
'''