summaryrefslogtreecommitdiff
path: root/src/plug.c
diff options
context:
space:
mode:
authoryuzu-eva <stevenhu@web.de>2024-12-16 20:59:26 +0100
committeryuzu-eva <stevenhu@web.de>2024-12-16 20:59:26 +0100
commit16f9ce1f005cd2d7c482722b961358d8f209bb83 (patch)
tree451a00f26bd29a2ba8318d96c9361c7d827df611 /src/plug.c
initial commit
Diffstat (limited to 'src/plug.c')
-rw-r--r--src/plug.c390
1 files changed, 390 insertions, 0 deletions
diff --git a/src/plug.c b/src/plug.c
new file mode 100644
index 0000000..fca3c7f
--- /dev/null
+++ b/src/plug.c
@@ -0,0 +1,390 @@
+#include <stdio.h>
+#include <math.h>
+#include <assert.h>
+#include <string.h>
+#include <stdlib.h>
+#include <complex.h>
+#include <raylib.h>
+#include <rlgl.h>
+
+#include "plug.h"
+
+#define SAMPLE_SIZE (1<<13)
+#define FONT_SIZE 69
+#define SMOOTHNESS 8
+#define SMEARNESS 6
+
+typedef struct {
+ Music music;
+ float audio_volume;
+ bool error;
+ Font font;
+ Shader circle;
+ Shader smear;
+ char render_option;
+} Plug;
+
+Plug *plug = NULL;
+
+float in_raw[SAMPLE_SIZE];
+float in_win[SAMPLE_SIZE];
+float complex out_raw[SAMPLE_SIZE];
+float out_log[SAMPLE_SIZE];
+float out_smooth[SAMPLE_SIZE];
+float out_smear[SAMPLE_SIZE];
+
+// char render_option = 'b';
+
+void fft(float in[], size_t stride, float complex out[], size_t n)
+{
+ assert(n > 0);
+
+ if (n == 1) {
+ out[0] = in[0];
+ return;
+ }
+
+ fft(in, stride*2, out, n/2); // even
+ fft(in + stride, stride*2, out + n/2, n/2); // odd
+
+ // v = o*x
+ // out = e + o*x e + o*x e e | e - o*x e - o*x o o
+ for (size_t k = 0; k < n/2; ++k) {
+ float t = (float)k/n;
+ float complex v = cexp(-2*I*PI*t)*out[k + n/2];
+ float complex e = out[k];
+ out[k] = e + v;
+ out[k + n/2] = e - v;
+ }
+}
+
+float amp(float complex z)
+{
+ float a = cabsf(z);
+ return 2*log10f(a);
+}
+
+void callback(void *bufferData, unsigned int frames)
+{
+ float (*fs)[2] = bufferData;
+
+ for (size_t i = 0; i < frames; ++i) {
+ memmove(in_raw, in_raw + 1, (SAMPLE_SIZE - 1)*sizeof(in_raw[0]));
+ in_raw[SAMPLE_SIZE-1] = fs[i][0];
+ }
+}
+
+void PausePlayMusic(void)
+{
+ if (IsMusicStreamPlaying(plug->music)) {
+ PauseMusicStream(plug->music);
+ printf("Music paused\n");
+ } else {
+ ResumeMusicStream(plug->music);
+ printf("Music resumed\n");
+ }
+}
+
+void RaiseVolume(void)
+{
+ plug->audio_volume += 0.02;
+ if (plug->audio_volume > 1.0) {
+ plug->audio_volume = 1.0;
+ }
+ printf("Audio Level: %f\n", plug->audio_volume);
+ SetMusicVolume(plug->music, plug->audio_volume);
+}
+
+void LowerVolume(void)
+{
+ plug->audio_volume -= 0.02;
+ if (plug->audio_volume < 0.0) {
+ plug->audio_volume = 0.0;
+ }
+ printf("Audio Level: %f\n", plug->audio_volume);
+ SetMusicVolume(plug->music, plug->audio_volume);
+}
+
+void PrepareMusicStream(void)
+{
+ if (IsMusicReady(plug->music)) {
+ StopMusicStream(plug->music);
+ UnloadMusicStream(plug->music);
+ }
+}
+
+void StartMusicStream(void)
+{
+ if (IsMusicReady(plug->music)) {
+ plug->error = false;
+ printf("music.frameCount = %u\n", plug->music.frameCount);
+ printf("music.stream.sampleRate = %u\n", plug->music.stream.sampleRate);
+ printf("music.stream.sampleSize = %u\n", plug->music.stream.sampleSize);
+ printf("music.stream.channels = %u\n", plug->music.stream.channels);
+ SetMusicVolume(plug->music, plug->audio_volume);
+ AttachAudioStreamProcessor(plug->music.stream, callback);
+ PlayMusicStream(plug->music);
+ } else {
+ plug->error = true;
+ }
+}
+
+size_t fft_analyze(float dt)
+{
+ // Apply Hann Window on input - https://en.wikipedia.org/wiki/Hann_function
+ for (size_t i = 0; i < SAMPLE_SIZE; ++i) {
+ float t = (float)i/(SAMPLE_SIZE-1);
+ float hann = 0.5 - 0.5*cosf(2*PI*t);
+ in_win[i] = hann * in_raw[i];
+ }
+
+ // FFT
+ fft(in_win, 1, out_raw, SAMPLE_SIZE);
+
+ // Convert into logarithmic scale
+ float step = 1.06;
+ float lowf = 1.0f;
+ size_t m = 0;
+ float max_amp = 1.0f;
+ for (float f = lowf; (size_t) f < SAMPLE_SIZE/2; f = ceilf(f*step)) {
+ float f1 = ceilf(f*step);
+ float a = 0.0f;
+ for (size_t q = (size_t) f; q < SAMPLE_SIZE/2 && q < (size_t) f1; ++q) {
+ float b = amp(out_raw[q]);
+ if (b > a) a = b;
+ }
+ if (max_amp < a) max_amp = a;
+ out_log[m++] = a;
+ }
+
+ // Normalize frequencies 0..1 range
+ for (size_t i = 0; i < m; ++i) {
+ out_log[i] /= max_amp;
+ }
+
+ // Interpolate frequencies
+ for (size_t i = 0; i < m; ++i) {
+ out_smooth[i] += (out_log[i] - out_smooth[i])*dt*SMOOTHNESS;
+ out_smear[i] += (out_smooth[i] - out_smear[i])*dt*SMEARNESS;
+ }
+ return m;
+}
+
+void fft_render_bars(int w, int h, size_t m)
+{
+ // Display the frequencies
+ float cell_width = (float)w/m;
+ float saturation = 0.75f;
+ float value = 0.9f;
+
+ // Draw the bars
+ for (size_t i = 0; i < m; ++i) {
+ float t = out_smooth[i];
+ float hue = (float)i/m;
+ Color color = ColorFromHSV(hue*360, saturation, value);
+ float thickness = (cell_width/1.8)*sqrtf(t);
+ Vector2 start_pos = {
+ .x = i*cell_width + cell_width/2,
+ .y = h,
+ };
+ Vector2 end_pos = {
+ .x = start_pos.x,
+ .y = start_pos.y - h*2/3*t,
+ };
+ DrawLineEx(start_pos, end_pos, thickness, color);
+ }
+
+
+ // Construct a texture to be used in the shader
+ // since fragTexCoord doesn't work on shapes
+ Texture2D texture = { rlGetTextureIdDefault(), 1, 1, 1, PIXELFORMAT_UNCOMPRESSED_R8G8B8A8 };
+
+ // Draw smear frames
+ BeginShaderMode(plug->smear);
+ for (size_t i = 0; i < m; ++i) {
+ float start = out_smear[i];
+ float end = out_smooth[i];
+ float hue = (float)i/m;
+ Color color = ColorFromHSV(hue*360, saturation, value);
+ float radius = cell_width*3*sqrt(end);
+ Vector2 origin = {0};
+ Vector2 start_pos = {
+ .x = i*cell_width + cell_width/2,
+ .y = h - h*2/3*start,
+ };
+ Vector2 end_pos = {
+ .x = i*cell_width + cell_width/2,
+ .y = h - h*2/3*end,
+ };
+ if (end_pos.y >= start_pos.y) {
+ Rectangle dest = {
+ .x = start_pos.x - radius/2,
+ .y = start_pos.y,
+ .width = radius,
+ .height = end_pos.y - start_pos.y,
+ };
+ Rectangle source = {0, 0, 1, 0.5};
+ DrawTexturePro(texture, source, dest, origin, 0, color);
+ } else {
+ Rectangle dest = {
+ .x = end_pos.x - radius/2,
+ .y = end_pos.y,
+ .width = radius,
+ .height = start_pos.y - end_pos.y,
+ };
+ Rectangle source = {0, 0.5, 1, 0.5};
+ DrawTexturePro(texture, source, dest, origin, 0, color);
+ }
+ }
+ EndShaderMode();
+
+ // Draw the circles
+ BeginShaderMode(plug->circle);
+ for (size_t i = 0; i < m; ++i) {
+ float t = out_smooth[i];
+ float hue = (float)i/m;
+ Color color = ColorFromHSV(hue*360, saturation, value);
+ float radius = cell_width*4*sqrtf(t);
+ Vector2 position = {
+ .x = i*cell_width + cell_width/2 - radius,
+ .y = h - h*2/3*t - radius,
+ };
+ DrawTextureEx(texture, position, 0, 2*radius, color);
+ }
+ EndShaderMode();
+}
+
+void fft_render_circle(int w, int h, size_t m)
+{
+ // Display the frequencies
+ float cell_width = (float)720/m;
+ float saturation = 0.75f;
+ float value = 0.9f;
+
+ // Construct a texture to be used in the shader
+ // since fragTexCoord doesn't work on shapes
+ Texture2D texture = { rlGetTextureIdDefault(), 1, 1, 1, PIXELFORMAT_UNCOMPRESSED_R8G8B8A8 };
+
+ // Draw the circles
+ BeginShaderMode(plug->circle);
+ for (size_t i = 0; i < m; ++i) {
+ float t = out_smooth[i];
+ float hue = (float)i/m;
+ Color color = ColorFromHSV(hue*360, saturation, value);
+ float radius = cell_width*4*sqrtf(t);
+ Vector2 position = {
+ .x = w/2 + (h/2.1 * cos(i * cell_width)) * sqrtf(t) - radius,
+ .y = h/2 + (h/2.1 * sin(i * cell_width)) * sqrtf(t) - radius,
+ };
+ DrawTextureEx(texture, position, 0, 2*radius, color);
+ }
+ EndShaderMode();
+}
+
+Plug *plug_pre_reload(void)
+{
+ if (IsMusicReady(plug->music)) {
+ DetachAudioStreamProcessor(plug->music.stream, callback);
+ }
+ return plug;
+}
+
+void plug_post_reload(Plug *prev)
+{
+ plug = prev;
+ if (IsMusicReady(plug->music)) {
+ AttachAudioStreamProcessor(plug->music.stream, callback);
+ }
+ UnloadShader(plug->circle);
+ UnloadShader(plug->smear);
+ plug->circle = LoadShader(NULL, "./shaders/circles.fs");
+ plug->smear = LoadShader(NULL, "./shaders/smear.fs");
+}
+
+void plug_init(void)
+{
+ plug = malloc(sizeof(*plug));
+ assert(plug != NULL && "Not enough RAM!");
+ memset(plug, 0, sizeof(*plug));
+
+ plug->font = LoadFontEx("./fonts/AlegreyaSans-Regular.ttf", FONT_SIZE, NULL, 0);
+ plug->circle = LoadShader(NULL, "./shaders/circles.fs");
+ plug->smear = LoadShader(NULL, "./shaders/smear.fs");
+ plug->audio_volume = 0.5f;
+ plug->render_option = 'b';
+}
+
+void plug_update(void)
+{
+ if (IsMusicReady(plug->music)) {
+ UpdateMusicStream(plug->music);
+ }
+
+ if (IsKeyPressed(KEY_SPACE) && IsMusicReady(plug->music)) {
+ PausePlayMusic();
+ }
+
+ if (IsKeyPressed(KEY_UP)) {
+ RaiseVolume();
+ }
+
+ if (IsKeyPressed(KEY_DOWN)) {
+ LowerVolume();
+ }
+
+ if (IsKeyPressed(KEY_N)) {
+ plug->render_option = 'b';
+ }
+
+ if (IsKeyPressed(KEY_I)) {
+ plug->render_option = 'c';
+ }
+
+ if (IsFileDropped()){
+ FilePathList droppedFiles = LoadDroppedFiles();
+ const char *file_path = droppedFiles.paths[0];
+
+ PrepareMusicStream();
+ plug->music = LoadMusicStream(file_path);
+ StartMusicStream();
+
+ UnloadDroppedFiles(droppedFiles);
+ }
+
+ float w = GetRenderWidth();
+ float h = GetRenderHeight();
+ float dt = GetFrameTime();
+
+ BeginDrawing();
+ ClearBackground(CLITERAL(Color) {
+ 0x1, 0x1, 0x1, 0xFF
+ });
+
+ if (IsMusicReady(plug->music)) {
+ size_t m = fft_analyze(dt);
+ switch(plug->render_option)
+ {
+ case 'b': fft_render_bars(w, h, m); break;
+ case 'c': fft_render_circle(w, h, m); break;
+ default: break;
+ }
+
+ } else {
+ const char *label;
+ Color color;
+ if (plug->error) {
+ label = "Could not load file";
+ color = RED;
+ } else {
+ label = "Drag & Drop Music Here";
+ color = WHITE;
+ }
+ Vector2 size = MeasureTextEx(plug->font, label, plug->font.baseSize, 0);
+ Vector2 position = {
+ .x = w/2 - size.x/2,
+ .y = h/2 - size.y/2,
+ };
+ DrawTextEx(plug->font, label, position, plug->font.baseSize, 0, color);
+ }
+ EndDrawing();
+}