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