Tyrolyean
72f3e51b7b
This receives the audio input, performs the fft on it and has all the nescessary preparations for then binding the audio data to the 2d texture for the GLSL. Signed-off-by: Tyrolyean <tyrolyean@tyrolyean.net>
452 lines
11 KiB
C
452 lines
11 KiB
C
/* SPDX-License-Identifier: ISC */
|
|
|
|
/*
|
|
* shadermeh.c
|
|
*
|
|
* Copyright (C) 2022 David Oberhollenzer <goliath@infraroot.at>
|
|
*/
|
|
#include "shadermeh.h"
|
|
#include <fcntl.h>
|
|
#include <errno.h>
|
|
#define HAVE_ARCH_STRUCT_FLOCK
|
|
#include <linux/fcntl.h>
|
|
#include <fftw3.h>
|
|
|
|
static GLfloat vertex_buffer[] = {
|
|
-1.0f, -1.0f, 0.0f, /* lower left corner */
|
|
1.0f, 1.0f, 1.0f,
|
|
|
|
+1.0f, -1.0f, 0.0f, /* lower right corner */
|
|
1.0f, 1.0f, 1.0f,
|
|
|
|
-1.0f, 1.0f, 0.0f, /* uper left corner */
|
|
1.0f, 1.0f, 1.0f,
|
|
|
|
+1.0f, +1.0f, 0.0f, /* upper right corner */
|
|
1.0f, 1.0f, 1.0f,
|
|
};
|
|
|
|
static double diff_timespec(const struct timespec *time1,
|
|
const struct timespec *time0)
|
|
{
|
|
return (time1->tv_sec - time0->tv_sec)
|
|
+ (time1->tv_nsec - time0->tv_nsec) / 1000000000.0;
|
|
}
|
|
|
|
static void convert_for_ffmpeg(const uint8_t *in, uint8_t *out,
|
|
size_t width, size_t height)
|
|
{
|
|
size_t x, y;
|
|
|
|
for (y = 0; y < height; ++y) {
|
|
const uint8_t *src = in + y * width * 4;
|
|
uint8_t *dst = out + (height - 1 - y) * width * 3;
|
|
|
|
for (x = 0; x < width; ++x) {
|
|
*(dst++) = *(src++);
|
|
*(dst++) = *(src++);
|
|
*(dst++) = *(src++);
|
|
++src;
|
|
}
|
|
}
|
|
}
|
|
|
|
static int write_retry(int fd, const void *buffer, size_t size)
|
|
{
|
|
while (size > 0) {
|
|
int ret = write(fd, buffer, size);
|
|
|
|
if (ret < 0) {
|
|
if (errno == EINTR){
|
|
continue;
|
|
}else if(errno == EAGAIN){
|
|
perror("Output pipe probably has O_NONBLOCK "
|
|
"set! Removing setting");
|
|
ret = fcntl(STDOUT_FILENO, F_SETFL,
|
|
fcntl(STDOUT_FILENO,F_GETFL)
|
|
&(~O_NONBLOCK));
|
|
if(ret == -1){
|
|
perror("Failed to set STDOUT blocking");
|
|
return -1;
|
|
}
|
|
continue;
|
|
}else{
|
|
perror("write");
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
if (ret == 0)
|
|
return -1;
|
|
|
|
size -= ret;
|
|
buffer = (const char *)buffer + ret;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
#define SND_BUFFER_SIZE 512
|
|
|
|
#define BBUFFER_S ((sizeof(float) * SND_BUFFER_SIZE))
|
|
|
|
/* Read all available input. This function presumes fd to be O_NONBLOCK */
|
|
static int read_audio_buffers(int fd, float buffer[SND_BUFFER_SIZE],
|
|
unsigned int sampling_rate){
|
|
|
|
float temp_buf[sampling_rate];
|
|
size_t n = 0;
|
|
memset(temp_buf, 0, sizeof(temp_buf));
|
|
while(true){
|
|
|
|
int new_n = read(fd, temp_buf, sizeof(temp_buf));
|
|
if(new_n < 0){
|
|
if(errno == EAGAIN || errno == EWOULDBLOCK){
|
|
break;
|
|
}
|
|
perror("Read to audio buffer failed");
|
|
return -1;
|
|
}else if(new_n == 0){
|
|
break;
|
|
} else if(n < sizeof(temp_buf)){
|
|
n = new_n;
|
|
break;
|
|
}
|
|
/* Fall through and read the remaining buffer
|
|
* (last ditch effort to clear pipe)
|
|
*/
|
|
}
|
|
if(n < BBUFFER_S){
|
|
memmove(buffer, buffer+n, BBUFFER_S - n);
|
|
memcpy(buffer+ BBUFFER_S - n, temp_buf, n);
|
|
}else{
|
|
memcpy(buffer, (temp_buf+n-BBUFFER_S), BBUFFER_S);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* normalize the given input buffer from -1…1 in range (what alsa provides) to
|
|
* 0…1 in range (what the shadertoy shaders */
|
|
static void normalize_audio_buffers(float buffer_in[SND_BUFFER_SIZE],
|
|
float buffer_out[SND_BUFFER_SIZE]){
|
|
|
|
if(buffer_in == NULL || buffer_out == NULL){
|
|
return;
|
|
}
|
|
|
|
for(size_t i = 0; i < SND_BUFFER_SIZE; i++){
|
|
buffer_out[i] = (buffer_in[i]+1.0)/2.0;
|
|
}
|
|
return;
|
|
}
|
|
|
|
static const struct option long_opts[] = {
|
|
{ "width", required_argument, NULL, 'w' },
|
|
{ "height", required_argument, NULL, 'h' },
|
|
{ "shader", required_argument, NULL, 's' },
|
|
{ "sampling-rate", required_argument, NULL, 'r' },
|
|
{ "to-stdout", no_argument, NULL, 'S' },
|
|
{ NULL, 0, NULL, 0 },
|
|
};
|
|
|
|
static const char *short_opts = "r:w:h:s:S";
|
|
|
|
static const char *usage_str =
|
|
"shadermeh OPTIONS...\n"
|
|
"\n"
|
|
"Possible options:\n"
|
|
"\n"
|
|
" --width, -w <pixels>\n"
|
|
" --height, -h <pixels>\n"
|
|
" --sampling-rate, -r <sampling rate[Hz]>\n"
|
|
"\n"
|
|
" --to-stdout, -S\n"
|
|
"\n"
|
|
" --shader, -s <shader file>\n"
|
|
"\n";
|
|
|
|
|
|
int main(int argc, char **argv)
|
|
{
|
|
GLuint u_iResolution, u_iTime, u_iTimeDelta, u_iFrame;
|
|
GLfloat u_iSampleRate;
|
|
struct timespec start, frame_start, frame_end;
|
|
unsigned int width, height, iFrame = 0;
|
|
void *fb32 = NULL, *fb24 = NULL;
|
|
const char *shader_file = NULL;
|
|
GLuint vao, vbo, fbo, fbo_tex;
|
|
GLint major, minor, prog;
|
|
float iTime, iTimeDelta;
|
|
bool to_stdout = false;
|
|
window *wnd;
|
|
int i, ret;
|
|
unsigned int sampling_rate = 0; /* Leaving this at 0 disables audio
|
|
* input
|
|
*/
|
|
float in_samples[SND_BUFFER_SIZE]; /* Raw input floats from -1...1 */
|
|
float norm_samples[SND_BUFFER_SIZE]; /* Normalized samples from 0...1 */
|
|
fftw_complex fftw_in[SND_BUFFER_SIZE];
|
|
fftw_complex fftw_out[SND_BUFFER_SIZE];
|
|
fftw_plan plan = fftw_plan_dft_1d(SND_BUFFER_SIZE,
|
|
fftw_in,
|
|
fftw_out,
|
|
FFTW_FORWARD,
|
|
FFTW_ESTIMATE);
|
|
|
|
/******************** parse options ************************/
|
|
width = 800;
|
|
height = 450;
|
|
|
|
for (;;) {
|
|
i = getopt_long(argc, argv, short_opts, long_opts, NULL);
|
|
if (i == -1)
|
|
break;
|
|
|
|
switch (i) {
|
|
case 'w':
|
|
width = strtol(optarg, NULL, 10);
|
|
break;
|
|
case 'h':
|
|
height = strtol(optarg, NULL, 10);
|
|
break;
|
|
case 's':
|
|
shader_file = optarg;
|
|
break;
|
|
case 'r':
|
|
sampling_rate = strtol(optarg, NULL, 10);
|
|
break;
|
|
case 'S':
|
|
to_stdout = true;
|
|
break;
|
|
default:
|
|
fputs(usage_str, stderr);
|
|
return EXIT_FAILURE;
|
|
}
|
|
}
|
|
|
|
if (!shader_file) {
|
|
fputs(usage_str, stderr);
|
|
fputs("No shader file specified!\n", stderr);
|
|
return EXIT_FAILURE;
|
|
}
|
|
|
|
if (to_stdout) {
|
|
fb32 = calloc(1, width * height * 4);
|
|
if (!fb32) {
|
|
perror("allocating scratch framebuffer");
|
|
return EXIT_FAILURE;
|
|
}
|
|
|
|
fb24 = calloc(1, width * height * 3);
|
|
if (!fb24) {
|
|
perror("allocating scratch framebuffer");
|
|
free(fb32);
|
|
return EXIT_FAILURE;
|
|
}
|
|
if(!isatty(STDOUT_FILENO)){
|
|
long old_pipe_size = fcntl(STDOUT_FILENO, F_GETPIPE_SZ);
|
|
if(old_pipe_size < 0){
|
|
perror("Failed to get stdout pipe size");
|
|
}else{
|
|
|
|
int psz = getpagesize();
|
|
size_t frame_size = (width * height * 3);
|
|
size_t frame_cnt = psz / frame_size;
|
|
if((psz % frame_size) != 0){
|
|
frame_cnt++;
|
|
}
|
|
size_t new_pipe_size = frame_size * frame_cnt;
|
|
int fin_psz = fcntl(STDOUT_FILENO, F_SETPIPE_SZ,
|
|
new_pipe_size);
|
|
if(fin_psz < 0){
|
|
fprintf(stderr, "Failed to set STDOUT "
|
|
"pipe size to %zu: %s\n",
|
|
new_pipe_size, strerror(errno));
|
|
}else{
|
|
fprintf(stderr, "stdout pipe size "
|
|
"change from %li to %zu "
|
|
"resulted in %i\n",
|
|
old_pipe_size,
|
|
new_pipe_size,
|
|
fin_psz);
|
|
}
|
|
/* A failure to change pipe size is not
|
|
* catastrophic, beacause the application will
|
|
* still perform as needed, just with a bigger
|
|
* or smaller buffer. */
|
|
}
|
|
}
|
|
}
|
|
if (sampling_rate != 0 && isatty(STDIN_FILENO)){
|
|
fputs("Sampling rate specified and STDIN a tty! "
|
|
"You habe been warnded!\n", stderr);
|
|
}
|
|
if(sampling_rate != 0){
|
|
ret = fcntl(STDIN_FILENO, F_SETFL,
|
|
fcntl(STDIN_FILENO,F_GETFL)
|
|
|(O_NONBLOCK));
|
|
if(ret == -1){
|
|
perror("Failed to set STDIN_FILENO to non-blocking "
|
|
"mode! This will likely mean failure");
|
|
}
|
|
}
|
|
|
|
/********** create window and make context current **********/
|
|
wnd = window_create(width, height, "shader meh...");
|
|
|
|
if (!wnd) {
|
|
fputs("failed to create window", stderr);
|
|
free(fb32);
|
|
free(fb24);
|
|
return EXIT_FAILURE;
|
|
}
|
|
|
|
window_make_current(wnd);
|
|
window_set_vsync(wnd, 1);
|
|
|
|
/******************** load entry points ********************/
|
|
glewExperimental = GL_TRUE;
|
|
|
|
if (glewInit() != GLEW_OK) {
|
|
fputs("glewInit() error", stderr);
|
|
window_destroy(wnd);
|
|
free(fb32);
|
|
free(fb24);
|
|
return EXIT_FAILURE;
|
|
}
|
|
|
|
glGetIntegerv(GL_MAJOR_VERSION, &major);
|
|
glGetIntegerv(GL_MINOR_VERSION, &minor);
|
|
fprintf(stderr,"OpenGL version %d.%d\n", major, minor);
|
|
|
|
/******************** initialization ********************/
|
|
glViewport(0, 0, width, height);
|
|
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
|
|
|
/* vertex buffer object & vertex array object */
|
|
glGenVertexArrays(1, &vao);
|
|
glBindVertexArray(vao);
|
|
|
|
glGenBuffers(1, &vbo);
|
|
glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
|
|
|
glBufferData(GL_ARRAY_BUFFER, sizeof(vertex_buffer),
|
|
vertex_buffer, GL_STATIC_DRAW);
|
|
|
|
glEnableVertexAttribArray(0);
|
|
glEnableVertexAttribArray(1);
|
|
|
|
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 6, 0);
|
|
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 6,
|
|
(GLvoid *)(3 * sizeof(GLfloat)));
|
|
|
|
/* shader */
|
|
prog = shader_program_load(shader_file);
|
|
|
|
if (!shader_program_get_build_status(prog)) {
|
|
shader_program_print_info_log(prog);
|
|
}
|
|
|
|
glUseProgram(prog);
|
|
|
|
/* uniforms */
|
|
u_iResolution = glGetUniformLocation(prog, "iResolution");
|
|
u_iTime = glGetUniformLocation(prog, "iTime");
|
|
u_iTimeDelta = glGetUniformLocation(prog, "iTimeDelta");
|
|
u_iFrame = glGetUniformLocation(prog, "iFrame;");
|
|
u_iSampleRate = glGetUniformLocation(prog, "iSampleRate");
|
|
|
|
glUniform3f(u_iResolution, width, height, 0.0f);
|
|
glUniform1f(u_iSampleRate, sampling_rate);
|
|
|
|
clock_gettime(CLOCK_MONOTONIC_RAW, &start);
|
|
|
|
/******************** framebuffer object ********************/
|
|
if (to_stdout) {
|
|
glGenFramebuffers(1, &fbo);
|
|
glGenTextures(1, &fbo_tex);
|
|
|
|
glBindTexture(GL_TEXTURE_2D, fbo_tex);
|
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0,
|
|
GL_RGBA, GL_UNSIGNED_BYTE, NULL);
|
|
|
|
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
|
glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
|
|
fbo_tex, 0);
|
|
} else {
|
|
window_show(wnd);
|
|
}
|
|
|
|
/******************** drawing loop ********************/
|
|
while (to_stdout || window_handle_events()) {
|
|
/* render image to FBO */
|
|
clock_gettime(CLOCK_MONOTONIC_RAW, &frame_start);
|
|
glClear(GL_COLOR_BUFFER_BIT);
|
|
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
|
|
|
if (to_stdout) {
|
|
glFlush();
|
|
} else {
|
|
window_swap_buffers(wnd);
|
|
}
|
|
|
|
/* get image from FBO, dump to stdout */
|
|
if (to_stdout) {
|
|
glGetTexImage(GL_TEXTURE_2D, 0, GL_RGBA,
|
|
GL_UNSIGNED_BYTE, fb32);
|
|
|
|
convert_for_ffmpeg(fb32, fb24, width, height);
|
|
|
|
if (write_retry(STDOUT_FILENO, fb24,
|
|
width * height * 3)) {
|
|
break;
|
|
}
|
|
}
|
|
if(sampling_rate != 0){
|
|
if(read_audio_buffers(STDIN_FILENO, in_samples,
|
|
sampling_rate) < 0){
|
|
break;
|
|
}
|
|
normalize_audio_buffers(in_samples, norm_samples);
|
|
memset(fftw_in, 0, sizeof(fftw_in));
|
|
memset(fftw_out, 0, sizeof(fftw_out));
|
|
for(size_t i = 0; i < SND_BUFFER_SIZE; i++){
|
|
fftw_in[i][0] = norm_samples[i];
|
|
}
|
|
fftw_execute(plan);
|
|
}
|
|
|
|
/* update timers */
|
|
clock_gettime(CLOCK_MONOTONIC_RAW, &frame_end);
|
|
|
|
iFrame += 1;
|
|
iTimeDelta = diff_timespec(&frame_end, &frame_start);
|
|
iTime = diff_timespec(&frame_end, &start);
|
|
|
|
glUniform1f(u_iTimeDelta, iTimeDelta);
|
|
glUniform1f(u_iTime, iTime);
|
|
glUniform1ui(u_iFrame, iFrame);
|
|
}
|
|
|
|
/******************** cleanup ********************/
|
|
glUseProgram(0);
|
|
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
|
glDeleteBuffers(1, &vbo);
|
|
glBindVertexArray(0);
|
|
glDeleteVertexArrays(1, &vao);
|
|
glDeleteProgram(prog);
|
|
|
|
if (to_stdout) {
|
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
|
glDeleteFramebuffers(1, &fbo);
|
|
glDeleteTextures(1, &fbo_tex);
|
|
}
|
|
|
|
window_make_current(NULL);
|
|
free(fb32);
|
|
free(fb24);
|
|
window_destroy(wnd);
|
|
fftw_destroy_plan(plan);
|
|
return EXIT_SUCCESS;
|
|
}
|