テラByteの時代にキロByte

shader又はdemosceneに関係する事

再)リアルタイム music shader (used transform feedback)

compute shaderで書いた奴を違うPCで試したら、使えなかった。どうなってるのだろう? なので、transform feedbackで書いた奴を載せておきます。きっと、こっちの方が安定してるかな。 そのうちにcompute shaderの方は調べてみます。

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.)); 
    //return fract(sin(t*45678.0)*1234.5)*2.0-1.0;
}

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))
    );
}
'''

src = '''
#version 330
uniform float phase;
uniform float bufferSize;
uniform float sampleRate; 
out vec2 gain; 
''' + shadertoy + '''
void main(){
  float time = (bufferSize * phase + float(gl_VertexID)) / sampleRate;
  gain=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.audioOutput = QAudioOutput(format, self)
        self.stream = self.audioOutput.start()
        self.audioOutput.setVolume(1)

    def initializeGL(self): 
        self.gl = QGLWidget()
        self.gl.makeCurrent()
        self.samples = (ctypes.c_float*self.bufferSize*self.channelCount)()
        self.variable = {
            "sampleRate": "sampleRate",
            "gain":       "gain",
            "bufferSize": "bufferSize",
            "phase":      "phase"
        }
        self.program = glCreateProgram()
        shader = glCreateShader(GL_VERTEX_SHADER)
        glShaderSource(shader, src)
        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)(self.variable["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)
        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)
        glUniform1f(glGetUniformLocation(self.program, self.variable["sampleRate"]), self.sampleRate)
        glUniform1f(glGetUniformLocation(self.program, self.variable["bufferSize"]), self.bufferSize)
        
    def update(self):
        if self.audioOutput.bytesFree() > 0:
            glUniform1f(glGetUniformLocation(self.program, self.variable["phase"]), self.phase)
            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 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_())