テラByteの時代にキロByte

shader又はdemosceneに関係する事

shaderでシンセサイザーへの一歩

pyside2にQDialを一つ配置。周波数を変えるダイヤル。任意のキーボード入力で発音。
とりあえずシンセの原形ができた。あとは、色々と試しながら遊ぶだけ。
音源を直接shaderにも書けるし、ツマミを使ってテストもできる。一つのscriptに機能を全部詰め込まないで、ちょっとした奴をたくさん作る方向に持って行く方が、面白くなる予感がする。

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

synthesize = '''
#version 330
uniform float bufferSize;
uniform float sampleRate; 

uniform float phase; 
uniform float gate; 
uniform float freq; 

out vec2 gain; 

#define OSC(f) square(f)
//#define OSC(f) Sin(f)

float Sin(float f)
{
    return sin(6.2831*f);
}

float square(float f)
{
  return sign(fract(f)-0.5);
}

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

void main(){
    float time = (bufferSize * phase + float(gl_VertexID)) / sampleRate;
    float gateTime = bufferSize * gate / sampleRate;
    //gain = vec2(OSC((440.0+freq)*time)) * step(time,gateTime);
    gain = vec2(OSC((440.0+freq)*time) * adsr(time,vec4(0.005, 0.08, 0.5, 0.1), gateTime));
}
'''

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):
        synthLayout = QHLayoutWidget(); self.layout.addWidget(synthLayout)
        self.freqDial = QDial();           synthLayout.addWidget(self.freqDial)
        self.freqDial.setSliderPosition(50)
        self.freqDial.valueChanged.connect(self.freqDial_valueChanged)
            
    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.variable = {
            "sampleRate": "sampleRate",
            "gain":       "gain",
            "bufferSize": "bufferSize",
            "phase":    "phase", 
            "gate":  "gate",
            "freq":  "freq"
        }
        self.phase = 10000
        self.gate = 0
        self.freq = 0

        self.program = glCreateProgram()
        shader = glCreateShader(GL_VERTEX_SHADER)
        glShaderSource(shader, synthesize)
        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)
        glUniform1f(glGetUniformLocation(self.program, self.variable["sampleRate"]), self.sampleRate)
        glUniform1f(glGetUniformLocation(self.program, self.variable["bufferSize"]), self.bufferSize)
        glUniform1f(glGetUniformLocation(self.program, self.variable["phase"]), self.phase)
        glUniform1f(glGetUniformLocation(self.program, self.variable["gate"]), self.gate)
        glUniform1f(glGetUniformLocation(self.program, self.variable["freq"]), self.freq)

        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() > self.bufferSize:
            glUniform1f(glGetUniformLocation(self.program, self.variable["phase"]), self.phase)
            glUniform1f(glGetUniformLocation(self.program, self.variable["gate"]), self.gate)
            glUniform1f(glGetUniformLocation(self.program, self.variable["freq"]), self.freq)
            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 freqDial_valueChanged(self, v):
        self.freq = (v - 50) * 30

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

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