#include #include #include #include #include #include #include #include #include #include #include #include #include #include "plug.h" #define SAMPLE_SIZE (1<<13) #define FONT_SIZE 69 #define SMOOTHNESS 8 #define SMEARNESS 6 char circle_shader[PATH_MAX]; char smear_shader[PATH_MAX]; char font[PATH_MAX]; 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'; char *get_asset_path(const char *asset, const char *type) { char *asset_path = malloc(sizeof(char) * PATH_MAX); char exe_path[PATH_MAX]; char exe_dir[PATH_MAX]; ssize_t len = readlink("/proc/self/exe", exe_path, sizeof(exe_path)); if (len == -1) { perror("readlink"); exit(EXIT_FAILURE); } exe_path[len] = '\0'; strncpy(exe_dir, exe_path, sizeof(exe_dir)); exe_dir[sizeof(exe_dir) - 1] = '\0'; char *dir = dirname(exe_dir); char rel_asset_base[PATH_MAX]; snprintf(rel_asset_base, sizeof(rel_asset_base), "%s/../share/%s", dir, type); char asset_base[PATH_MAX]; if (!realpath(rel_asset_base, asset_base)) { fprintf(stderr, "ERROR: realpath failed for asset %s\n", strerror(errno)); exit(EXIT_FAILURE); } if ((size_t)snprintf(asset_path, PATH_MAX, "%s/%s", asset_base, asset) >= PATH_MAX) { fprintf(stderr, "Asset path truncated -- path too long\n"); exit(EXIT_FAILURE); } return asset_path; } 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 (IsMusicValid(plug->music)) { StopMusicStream(plug->music); UnloadMusicStream(plug->music); } } void StartMusicStream(void) { if (IsMusicValid(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 (IsMusicValid(plug->music)) { DetachAudioStreamProcessor(plug->music.stream, callback); } return plug; } void plug_post_reload(Plug *prev) { plug = prev; if (IsMusicValid(plug->music)) { AttachAudioStreamProcessor(plug->music.stream, callback); } UnloadShader(plug->circle); UnloadShader(plug->smear); plug->circle = LoadShader(NULL, circle_shader); plug->smear = LoadShader(NULL, smear_shader); } void plug_init(void) { plug = malloc(sizeof(*plug)); assert(plug != NULL && "Not enough RAM!"); memset(plug, 0, sizeof(*plug)); char *font_path = get_asset_path("AlegreyaSans-Regular.ttf", "fonts"); char *circles_path = get_asset_path("circles.fs", "shaders"); char *smear_path = get_asset_path("smear.fs", "shaders"); plug->font = LoadFontEx(font_path, FONT_SIZE, NULL, 0); plug->circle = LoadShader(NULL, circles_path); plug->smear = LoadShader(NULL, smear_path); plug->audio_volume = 0.5f; plug->render_option = 'b'; free(font_path); free(circles_path); free(smear_path); } void plug_update(void) { if (IsMusicValid(plug->music)) { UpdateMusicStream(plug->music); } if (IsKeyPressed(KEY_SPACE) && IsMusicValid(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 (IsMusicValid(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(); }