commit cdd60c0036503df6abdabfa035a35bb5ea17e905 Author: iceyrazor Date: Tue Feb 24 03:11:57 2026 -0600 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a134e06 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +audio_reactive +src/.clangd diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..014a60c --- /dev/null +++ b/LICENSE @@ -0,0 +1,11 @@ +Copyright 2026 iceyrazor + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..86789bf --- /dev/null +++ b/README.md @@ -0,0 +1,40 @@ +# Audio Reactive + +![audio reactive screenshot](./example.png?raw=true) + +This is a small C SDL2 project making physics objects react to audio + +## Requirements + +- SDL2 +- SDL2_gfx +- libpipewire +- libspa-2.0 +- clang +- libgcc +- pkg-config + +## building + +simply run ./make +there is no install + +## usage + +You can specify the reading device. I have no clue how it works. + +Just run audio_reactive binary. +Then use something like qpwgraph to to force change the input of ``audio-capture`` + +## Todo + +- [ ] resizeable +- [ ] arguments +- [ ] settable average peak +- [ ] settable RGB +- [ ] RGB-cycle +- [ ] move settable vars to stuff +- [ ] ability to set input device with args +- [ ] volume of frequency instead of overall +- [ ] random kick based on low frequency peak instead of volume +- [ ] graceful entrance and exit entering or exiting volume 0 diff --git a/example.png b/example.png new file mode 100644 index 0000000..c26708d Binary files /dev/null and b/example.png differ diff --git a/make b/make new file mode 100755 index 0000000..2fcfe88 --- /dev/null +++ b/make @@ -0,0 +1,17 @@ +#!/bin/sh + +DEBUG="" +LIB="-Ilib/ -lSDL2_gfx $(pkg-config --cflags --libs libpipewire-0.3) -I/usr/include/spa-0.2" + +while getopts "d" opt; do + case "$opt" in + d) DEBUG="-Wall -fsanitize=address -g" + ;; + esac +done + +shift $((OPTIND-1)) +[ "${1:-}" = "--" ] && shift + +printf "\n\nmaking project\n" +clang -o "audio_reactive" src/main.c $LIB `sdl2-config --cflags --libs` -lm $DEBUG diff --git a/src/global_objects.c b/src/global_objects.c new file mode 100644 index 0000000..2e3f2fd --- /dev/null +++ b/src/global_objects.c @@ -0,0 +1,72 @@ +#ifndef global_objects +#define global_objects + +#include +#include + +#define flaot float + +typedef struct { + float x; + float y; +} Point; + +void point_add(Point *A, Point *B){ + A->x = A->x + B->x; + A->y = A->y + B->y; +} + +void point_sub(Point *A, Point *B){ + A->x = A->x - B->x; + A->y = A->y - B->y; +} + +void point_mul(Point *A, Point *B){ + A->x = A->x * B->x; + A->y = A->y * B->y; +} + +void point_div(Point *A, Point *B){ + A->x = A->x / B->x; + A->y = A->y / B->y; +} + +float magnitude(flaot x, flaot y) { + return sqrt((pow(x,2) + pow(y,2))); +} + +void point_set_mag(Point *point, float mag){ + flaot getmag = magnitude(point->x, point->y); + flaot rx = (point->x / getmag) * mag; + flaot ry = (point->y / getmag) * mag; + + point->x = rx; + point->y = ry; +} + +void point_limit(Point *point, float mag){ + flaot getmag = magnitude(point->x, point->y); + flaot rx, ry; + if (getmag > mag){ + rx = (point->x / getmag) * mag; + ry = (point->y / getmag) * mag; + } else { + rx = point->x; + ry = point->y; + } + + point->x = rx; + point->y = ry; +} + +float RandomFloat(float min, float max){ + return ((max - min) * ((float)rand() / (double)RAND_MAX)) + min; +} + +float constrain(float val, float min, float max){ + if(val > max) val=max; + else if(val < min) val=min; + return val; +} + +#endif diff --git a/src/main.c b/src/main.c new file mode 100755 index 0000000..6e3dd89 --- /dev/null +++ b/src/main.c @@ -0,0 +1,107 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +bool running=true; + +#include "objects.h" +#include "objects.c" +#include "pipe.c" + +#define PROJECT_NAME "audio reactive" + + +int main(int argc, char *argv[]) +{ + if (SDL_InitSubSystem(SDL_INIT_VIDEO) < 0) + SDL_Log("SDL fails to initialize! %s\n", SDL_GetError()); + + int startw=600; + int starth=600; + + SDL_Window *window = SDL_CreateWindow(PROJECT_NAME, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, startw, starth, SDL_WINDOW_SHOWN | SDL_WINDOW_OPENGL); + if (!window) { + printf("Failed to create window: %s\n", SDL_GetError()); + } + SDL_Renderer *renderer = SDL_CreateRenderer(window,-1,SDL_RENDERER_SOFTWARE); + if (!renderer) { + printf("Failed to create renderer: %s\n", SDL_GetError()); + } + + VOL_ARG volargs={ argc, argv, }; + pthread_t vol_thread; + int thread_err=pthread_create(&vol_thread, NULL, volume_input, (void *)&volargs); + + pthread_t peak_reduc; + int thread_err2=pthread_create(&peak_reduc, NULL, avg_peak, NULL); + + srand(time(NULL)); + OBJECT_INER obj_storage[obj_n]; + + STUFFS stuff = { + startw, + starth, + window, + renderer, + false, + 0, + 0, + (OBJECT){}, + 0, + 0, + }; + + init_stuffs(startw,starth,obj_storage,&stuff); + + + for (int i = 0; i < obj_n; i++) + stuff.obj.objs[i] = &obj_storage[i]; + + SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND); + + bool quit = false; + SDL_Event e; + while (!quit) { + while (SDL_PollEvent(&e)) { + if (e.type == SDL_QUIT) { + quit = true; + } + else if(e.type == SDL_MOUSEBUTTONDOWN){ + if(e.button.button==SDL_BUTTON_LEFT){ + stuff.mousedown=true; + } else if(e.button.button==SDL_BUTTON_RIGHT){ + } + } else if(e.type==SDL_MOUSEBUTTONUP){ + stuff.mousedown=false; + } + stuff.mx=e.button.x; + stuff.my=e.button.y; + } + if(mutVolume.locked==false){ + mutVolume.locked=true; + stuff.volume=mutVolume.volume; + stuff.avg_peak=mutVolume.avg_peak; + mutVolume.locked=false; + } + //SDL_Log("vol: %f avg: %f",stuff.volume,stuff.avg_peak); + draw(&stuff); + SDL_Delay(5); + } + + running=false; + pw_main_loop_quit(GPipe.d->loop); + pthread_join(vol_thread,NULL); + pthread_join(peak_reduc,NULL); + + SDL_DestroyRenderer(renderer); + SDL_DestroyWindow(window); + SDL_QuitSubSystem(SDL_INIT_VIDEO); + SDL_Quit(); + + return 0; +} diff --git a/src/objects.c b/src/objects.c new file mode 100755 index 0000000..1107c1d --- /dev/null +++ b/src/objects.c @@ -0,0 +1,122 @@ +#include +#include +#include +#include + +#include "global_objects.c" +#include "objects.h" + +void constructor(int w, int h, OBJECT_INER *obj, float x, float y, float mass){ + obj->pos.x = x; + obj->pos.y = y; + obj->vel = (Point){RandomFloat(-1, 1),RandomFloat(-1, 1)}; + point_mul(&obj->vel,&(Point){5,5}); + obj->acc = (Point){0,0}; + obj->mass = mass; + obj->r = sqrt(obj->mass) * 2; + obj->retur=false; +} + +void attractor_constructor(int w, int h, ATTRACTOR *attractor, float x, float y, float mass){ + attractor->pos.x = x; + attractor->pos.y = y; + attractor->mass = mass; + attractor->r = sqrt(attractor->mass) * 2; +} + +void init_stuffs(int w, int h, OBJECT_INER *objs,STUFFS *stuff){ + for (int i=0; iobj.attractor,(float)w/2,(float)h/2,100); +} + +void applyForce(OBJECT_INER *obj, Point force){ + point_div(&force, &(Point){obj->mass,obj->mass}); + point_add(&obj->acc, &force); +} + +void update(STUFFS *stuff, OBJECT_INER *obj){ + point_add(&obj->vel,&obj->acc); + point_limit(&obj->vel,10); + point_add(&obj->pos, &obj->vel); + obj->acc = (Point){0,0}; +} + +void show(STUFFS *stuff,OBJECT_INER *obj){ + Sint16 x = (Sint16)obj->pos.x; + Sint16 y = (Sint16)obj->pos.y; + + circleRGBA(stuff->renderer, x, y, obj->r, 190, 90, 255, 255); + filledCircleRGBA(stuff->renderer, x, y, obj->r, 150, 50, 255, 80); +} + +float calcG(ATTRACTOR *attractor, OBJECT_INER *obj,STUFFS *stuff){ + float G=0; + float dis=0; + Point pointA=attractor->pos; + point_sub(&pointA,&obj->pos); + dis=magnitude(pointA.x, pointA.y); + + if (dis>(float)stuff->width/3){ + obj->retur=true; + } + else if (dis<(float)stuff->width/3){ + obj->retur=false; + } + + G=1-(stuff->volume)*15; + + if(obj->retur==true) + G=stuff->volume*20; + + if(stuff->volume<0.01){ + G=5; + } + + if(G!=G) G=1; + return G; +} + +void attract(ATTRACTOR *attractor, OBJECT_INER *obj,STUFFS *stuff){ + Point force = {attractor->pos.x - obj->pos.x, attractor->pos.y - obj->pos.y}; + float dist = magnitude(force.x, force.y); + dist = dist * dist; + dist = constrain(dist,100,1000); + + float G = calcG(attractor,obj,stuff); + float strength = G * (attractor->mass * obj->mass) / dist; + + point_set_mag(&force, strength); + applyForce(obj,force); +} + +void atr_show(STUFFS *stuff,ATTRACTOR *attr){ + SDL_RenderFillRect(stuff->renderer, &(SDL_Rect){attr->pos.x - attr->r, attr->pos.y - attr->r, attr->r*2, attr->r*2}); +} + +void draw(STUFFS *stuff){ + SDL_SetRenderDrawColor(stuff->renderer,0,0,0,30); + //SDL_RenderClear(renderer); + SDL_RenderFillRect(stuff->renderer, &(SDL_Rect){0,0,stuff->width,stuff->height}); + static int frame_i1=0; + + for (int i=0; iobj.objs[i]); + show(stuff,stuff->obj.objs[i]); + + attract(&stuff->obj.attractor, stuff->obj.objs[i], stuff); + float vol=stuff->volume; + if(vol==vol && frame_i1>200){ + Point rand_p={RandomFloat(-1, 1),RandomFloat(-1, 1)}; + point_set_mag(&rand_p, stuff->volume*100); + point_add(&stuff->obj.objs[i]->vel,&rand_p); + } + } + SDL_SetRenderDrawColor(stuff->renderer, 100,20,20,180); + atr_show(stuff,&stuff->obj.attractor); + + if(frame_i1>200) frame_i1=0; + frame_i1++; + SDL_RenderPresent(stuff->renderer); +} diff --git a/src/objects.h b/src/objects.h new file mode 100755 index 0000000..d1faa02 --- /dev/null +++ b/src/objects.h @@ -0,0 +1,52 @@ +#ifndef objects +#define objects + +#include +#include +#include +#include "global_objects.c" + +#define flaot float + +#define obj_n 30 + +typedef struct ATTRACTOR { + Point pos; + float r; + float mass; +} ATTRACTOR; + +typedef struct OBJECT_INER { + Point pos; + Point vel; + Point acc; + float r; + float mass; + bool retur; +} OBJECT_INER; + +typedef struct OBJECT { + ATTRACTOR attractor; + OBJECT_INER *objs[obj_n]; +} OBJECT; + +typedef struct{ + bool locked; + flaot volume; + flaot avg_peak; +} MUTEX_FLOAT; + +typedef struct{ + int width; + int height; + SDL_Window* window; + SDL_Renderer* renderer; + bool mousedown; + int mx; + int my; + OBJECT obj; + flaot volume; + flaot avg_peak; +} STUFFS; + +#endif diff --git a/src/pipe.c b/src/pipe.c new file mode 100644 index 0000000..60824ea --- /dev/null +++ b/src/pipe.c @@ -0,0 +1,239 @@ +#ifndef pipec +#define pipec +#include +#include +#include +#include "objects.h" + +MUTEX_FLOAT mutVolume = { false, 0, }; + +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +/* + [title] + Audio capture using \ref pw_stream "pw_stream". + [title] + */ + +struct data { + struct pw_main_loop *loop; + struct pw_stream *stream; + + struct spa_audio_info format; + unsigned move:1; +}; + +/* our data processing function is in general: + * + * struct pw_buffer *b; + * b = pw_stream_dequeue_buffer(stream); + * + * .. consume stuff in the buffer ... + * + * pw_stream_queue_buffer(stream, b); + */ +static void on_process(void *userdata) +{ + struct data *data = userdata; + struct pw_buffer *b; + struct spa_buffer *buf; + float *samples, max; + uint32_t c, n, n_channels, n_samples, peak; + + if ((b = pw_stream_dequeue_buffer(data->stream)) == NULL) { + pw_log_warn("out of buffers: %m"); + return; + } + + buf = b->buffer; + if ((samples = buf->datas[0].data) == NULL) + return; + + n_channels = data->format.info.raw.channels; + n_samples = buf->datas[0].chunk->size / sizeof(float); + + /* move cursor up */ + //if (data->move) + // fprintf(stdout, "%c[%dA", 0x1b, n_channels + 1); + //fprintf(stdout, "captured %d samples\n", n_samples / n_channels); + + for (c = 0; c < data->format.info.raw.channels; c++) { + max = 0.0f; + for (n = c; n < n_samples; n += n_channels) + max = fmaxf(max, fabsf(samples[n])); + + peak = (uint32_t)SPA_CLAMPF(max * 30, 0.f, 39.f); + + mutVolume.locked=true; + if(mutVolume.avg_peakmove = true; + fflush(stdout); + + pw_stream_queue_buffer(data->stream, b); +} + +/* Be notified when the stream param changes. We're only looking at the + * format changes. + */ +static void +on_stream_param_changed(void *_data, uint32_t id, const struct spa_pod *param) +{ + struct data *data = _data; + + /* NULL means to clear the format */ + if (param == NULL || id != SPA_PARAM_Format) + return; + + if (spa_format_parse(param, &data->format.media_type, &data->format.media_subtype) < 0) + return; + + /* only accept raw audio */ + if (data->format.media_type != SPA_MEDIA_TYPE_audio || + data->format.media_subtype != SPA_MEDIA_SUBTYPE_raw) + return; + + /* call a helper function to parse the format for us. */ + spa_format_audio_raw_parse(param, &data->format.info.raw); + + //fprintf(stdout, "capturing rate:%d channels:%d\n", + // data->format.info.raw.rate, data->format.info.raw.channels); + +} + +static const struct pw_stream_events stream_events = { + PW_VERSION_STREAM_EVENTS, + .param_changed = on_stream_param_changed, + .process = on_process, +}; + +static void do_quit(void *userdata, int signal_number) +{ + struct data *data = userdata; + pw_main_loop_quit(data->loop); +} + +typedef struct VOL_ARG{ + int argc; + char **argv; +} VOL_ARG; + +struct pipe_control{ + struct data* d; +}; + +struct pipe_control GPipe; + +void *volume_input(void* threadarg){ + struct VOL_ARG *volargs; + volargs=(struct VOL_ARG *) threadarg; + + int argc=volargs->argc; + + struct data data = { 0, }; + GPipe.d=&data; + const struct spa_pod *params[1]; + uint8_t buffer[1024]; + struct pw_properties *props; + struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); + + pw_init(&argc, &volargs->argv); + + /* make a main loop. If you already have another main loop, you can add + * the fd of this pipewire mainloop to it. */ + data.loop = pw_main_loop_new(NULL); + + pw_loop_add_signal(pw_main_loop_get_loop(data.loop), SIGINT, do_quit, &data); + pw_loop_add_signal(pw_main_loop_get_loop(data.loop), SIGTERM, do_quit, &data); + + /* Create a simple stream, the simple stream manages the core and remote + * objects for you if you don't need to deal with them. + * + * If you plan to autoconnect your stream, you need to provide at least + * media, category and role properties. + * + * Pass your events and a user_data pointer as the last arguments. This + * will inform you about the stream state. The most important event + * you need to listen to is the process event where you need to produce + * the data. + */ + props = pw_properties_new(PW_KEY_MEDIA_TYPE, "Audio", + PW_KEY_MEDIA_CATEGORY, "Capture", + PW_KEY_MEDIA_ROLE, "Music", + NULL); + if (argc > 1) + /* Set stream target if given on command line */ + pw_properties_set(props, PW_KEY_TARGET_OBJECT, volargs->argv[1]); + + /* uncomment if you want to capture from the sink monitor ports */ + /* pw_properties_set(props, PW_KEY_STREAM_CAPTURE_SINK, "true"); */ + + data.stream = pw_stream_new_simple( + pw_main_loop_get_loop(data.loop), + "audio-capture", + props, + &stream_events, + &data); + + /* Make one parameter with the supported formats. The SPA_PARAM_EnumFormat + * id means that this is a format enumeration (of 1 value). + * We leave the channels and rate empty to accept the native graph + * rate and channels. */ + params[0] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat, + &SPA_AUDIO_INFO_RAW_INIT( + .format = SPA_AUDIO_FORMAT_F32)); + + /* Now connect this stream. We ask that our process function is + * called in a realtime thread. */ + pw_stream_connect(data.stream, + PW_DIRECTION_INPUT, + PW_ID_ANY, + PW_STREAM_FLAG_AUTOCONNECT | + PW_STREAM_FLAG_MAP_BUFFERS | + PW_STREAM_FLAG_RT_PROCESS, + params, 1); + + /* and wait while we let things run */ + pw_main_loop_run(data.loop); + + pw_stream_destroy(data.stream); + pw_main_loop_destroy(data.loop); + pw_deinit(); + + pthread_exit(NULL); +} + +void delay(int milli_seconds) +{ + // Storing start time + clock_t start_time = clock(); + + // looping till required time is not achieved + while (clock() < start_time + milli_seconds); +} + +void *avg_peak(){ + while (running){ + if(mutVolume.locked==false){ + mutVolume.locked=true; + if(mutVolume.avg_peak>0){ + mutVolume.avg_peak=mutVolume.avg_peak-0.0001; + } + mutVolume.locked=false; + } + delay(100); + } + pthread_exit(NULL); +} + +#endif