← ziglang.inraylib-zigGitHub

spectrum visualizer

★★★☆by IANN (@meisei4)

There is no browser build of this example yet.

Is it the same as C?

//! raylib [audio] example - spectrum visualizer
//!
//! Zig-API translation of examples/audio/audio_spectrum_visualizer.c (raylib 6.2).
//! Inspired by Inigo Quilez's https://www.shadertoy.com/
//! Resources/specification: https://gist.github.com/soulthreads/2efe50da4be1fb5f7ab60ff14ca434b8
//! Example created by created by IANN (@meisei4) reviewed by Ramon Santamaria (@raysan5)
//! Example licensed under an unmodified zlib/libpng license.
//! Copyright (c) 2025 IANN (@meisei4)

const std = @import("std");
const rl = @import("raylib");

const glsl_version = 330;

const mono = 1;
const sample_rate = 44100;
const sample_rate_f: f32 = 44100.0;
const fft_window_size = 1024;
const buffer_size = 512;
const per_sample_bit_depth = 16;
const audio_stream_ring_buffer_size = fft_window_size * 2;
const effective_sample_rate: f32 = sample_rate_f * 0.5;
const window_time: f64 = @as(f64, fft_window_size) / @as(f64, effective_sample_rate);
const fft_historical_smoothing_dur: f32 = 2.0;
const min_decibels: f32 = -100.0; // https://developer.mozilla.org/en-US/docs/Web/API/AnalyserNode/minDecibels
const max_decibels: f32 = -30.0; // https://developer.mozilla.org/en-US/docs/Web/API/AnalyserNode/maxDecibels
const inverse_decibel_range: f32 = 1.0 / (max_decibels - min_decibels);
const db_to_linear_scale: f32 = 20.0 / @as(f32, 2.302585092994046);
const smoothing_time_constant: f32 = 0.8; // https://developer.mozilla.org/en-US/docs/Web/API/AnalyserNode/smoothingTimeConstant
const texture_height = 1;
const fft_row = 0;
const unused_channel: f32 = 0.0;

const fft_history_len: comptime_int = @as(i32, @intFromFloat(@ceil(@as(f32, @floatCast(fft_historical_smoothing_dur / window_time))))) + 1;

const FftComplex = struct { real: f32, imaginary: f32 };

const FftData = struct {
    spectrum: [fft_window_size]FftComplex = @splat(.{ .real = 0, .imaginary = 0 }),
    work_buffer: [fft_window_size]FftComplex = @splat(.{ .real = 0, .imaginary = 0 }),
    prev_magnitudes: [buffer_size]f32 = @splat(0),
    fft_history: [fft_history_len][buffer_size]f32 = @splat(@splat(0)),
    history_pos: usize = 0,
    last_fft_time: f64 = 0.0,
    tapback_pos: f32 = 0.01,
};

pub fn main() !void {
    // Initialization
    const screen_width = 800;
    const screen_height = 450;

    rl.initWindow(screen_width, screen_height, "raylib [audio] example - spectrum visualizer");
    defer rl.closeWindow(); // Close window and OpenGL context

    var fft_image = rl.genImageColor(buffer_size, texture_height, .white);
    defer rl.unloadImage(fft_image);
    const fft_texture = try rl.loadTextureFromImage(fft_image);
    defer rl.unloadTexture(fft_texture);
    const buffer_a = try rl.loadRenderTexture(screen_width, screen_height);
    defer rl.unloadRenderTexture(buffer_a);
    const i_resolution: rl.Vector2 = .{ .x = screen_width, .y = screen_height };

    const shader = try rl.loadShader(null, std.fmt.comptimePrint("resources/shaders/glsl{d}/fft.fs", .{glsl_version}));
    defer rl.unloadShader(shader);

    const i_resolution_location = rl.getShaderLocation(shader, "iResolution");
    const i_channel0_location = rl.getShaderLocation(shader, "iChannel0");
    rl.setShaderValue(shader, i_resolution_location, i_resolution);
    rl.setShaderValueTexture(shader, i_channel0_location, fft_texture);

    rl.initAudioDevice();
    defer rl.closeAudioDevice();
    rl.setAudioStreamBufferSizeDefault(audio_stream_ring_buffer_size);

    var wav = try rl.loadWave("resources/country.mp3");
    defer rl.unloadWave(wav);
    rl.waveFormat(&wav, sample_rate, per_sample_bit_depth, mono);

    const audio_stream = try rl.loadAudioStream(sample_rate, per_sample_bit_depth, mono);
    defer rl.unloadAudioStream(audio_stream);
    rl.playAudioStream(audio_stream);

    var fft: FftData = .{};

    const wav_pcm16 = @as([*]const i16, @ptrCast(@alignCast(wav.data.?)))[0 .. wav.frameCount * wav.channels];
    var wav_cursor: usize = 0;

    var chunk_samples: [audio_stream_ring_buffer_size]i16 = @splat(0);
    var audio_samples: [fft_window_size]f32 = @splat(0);

    rl.setTargetFPS(60);

    // Main game loop
    while (!rl.windowShouldClose()) { // Detect window close button or ESC key
        // Update
        while (rl.isAudioStreamProcessed(audio_stream)) {
            for (&chunk_samples) |*sample| {
                const left: i32 = if (wav.channels == 2) wav_pcm16[wav_cursor * 2 + 0] else wav_pcm16[wav_cursor];
                const right: i32 = if (wav.channels == 2) wav_pcm16[wav_cursor * 2 + 1] else left;
                sample.* = @intCast(@divTrunc(left + right, 2));

                wav_cursor += 1;
                if (wav_cursor >= wav.frameCount) wav_cursor = 0;
            }

            rl.updateAudioStream(audio_stream, &chunk_samples, audio_stream_ring_buffer_size);

            for (&audio_samples, 0..) |*sample, i| {
                const sum: f32 = @floatFromInt(@as(i32, chunk_samples[i * 2]) + chunk_samples[i * 2 + 1]);
                sample.* = sum * 0.5 / 32767.0;
            }
        }

        captureFrame(&fft, &audio_samples);
        renderFrame(&fft, &fft_image);
        rl.updateTexture(fft_texture, fft_image.data.?);

        // Draw
        rl.beginDrawing();
        defer rl.endDrawing();

        rl.clearBackground(.ray_white);

        {
            rl.beginShaderMode(shader);
            defer rl.endShaderMode();

            rl.setShaderValueTexture(shader, i_channel0_location, fft_texture);
            rl.drawTextureRec(
                buffer_a.texture,
                .{ .x = 0, .y = 0, .width = screen_width, .height = -screen_height },
                .{ .x = 0, .y = 0 },
                .white,
            );
        }
    }
}

/// Cooley–Tukey FFT https://en.wikipedia.org/wiki/Cooley%E2%80%93Tukey_FFT_algorithm#Data_reordering,_bit_reversal,_and_in-place_algorithms
fn cooleyTukeyFftSlow(spectrum: []FftComplex) void {
    const n = spectrum.len;

    var j: usize = 0;
    for (1..n - 1) |i| {
        var bit = n >> 1;
        while (j >= bit) {
            j -= bit;
            bit >>= 1;
        }
        j += bit;
        if (i < j) std.mem.swap(FftComplex, &spectrum[i], &spectrum[j]);
    }

    var len: usize = 2;
    while (len <= n) : (len <<= 1) {
        const angle = -2.0 * rl.pi / @as(f32, @floatFromInt(len));
        const twiddle_unit: FftComplex = .{ .real = @cos(angle), .imaginary = @sin(angle) };
        const half = len / 2;
        var i: usize = 0;
        while (i < n) : (i += len) {
            var twiddle_current: FftComplex = .{ .real = 1.0, .imaginary = 0.0 };
            for (spectrum[i..][0..half], spectrum[i + half ..][0..half]) |*even_slot, *odd_slot| {
                const even = even_slot.*;
                const odd = odd_slot.*;
                const twiddled_odd: FftComplex = .{
                    .real = odd.real * twiddle_current.real - odd.imaginary * twiddle_current.imaginary,
                    .imaginary = odd.real * twiddle_current.imaginary + odd.imaginary * twiddle_current.real,
                };

                even_slot.* = .{ .real = even.real + twiddled_odd.real, .imaginary = even.imaginary + twiddled_odd.imaginary };
                odd_slot.* = .{ .real = even.real - twiddled_odd.real, .imaginary = even.imaginary - twiddled_odd.imaginary };

                const t = twiddle_current;
                twiddle_current = .{
                    .real = t.real * twiddle_unit.real - t.imaginary * twiddle_unit.imaginary,
                    .imaginary = t.real * twiddle_unit.imaginary + t.imaginary * twiddle_unit.real,
                };
            }
        }
    }
}

fn captureFrame(fft_data: *FftData, audio_samples: *const [fft_window_size]f32) void {
    for (&fft_data.work_buffer, audio_samples, 0..) |*bin, sample, i| {
        const x = (2.0 * rl.pi * @as(f32, @floatFromInt(i))) / (fft_window_size - 1.0);
        const blackman_weight = 0.42 - 0.5 * @cos(x) + 0.08 * @cos(2.0 * x); // https://en.wikipedia.org/wiki/Window_function#Blackman_window
        bin.* = .{ .real = sample * blackman_weight, .imaginary = 0.0 };
    }

    cooleyTukeyFftSlow(&fft_data.work_buffer);
    fft_data.spectrum = fft_data.work_buffer;

    var smoothed_spectrum: [buffer_size]f32 = undefined;

    for (&smoothed_spectrum, &fft_data.prev_magnitudes, fft_data.work_buffer[0..buffer_size]) |*smoothed, *prev_magnitude, bin| {
        const linear_magnitude = @sqrt(bin.real * bin.real + bin.imaginary * bin.imaginary) / fft_window_size;

        const smoothed_magnitude = smoothing_time_constant * prev_magnitude.* + (1.0 - smoothing_time_constant) * linear_magnitude;
        prev_magnitude.* = smoothed_magnitude;

        const db = @log(@max(smoothed_magnitude, 1e-40)) * db_to_linear_scale;
        const normalized = (db - min_decibels) * inverse_decibel_range;
        smoothed.* = rl.math.clamp(normalized, 0.0, 1.0);
    }

    fft_data.last_fft_time = rl.getTime();
    fft_data.fft_history[fft_data.history_pos] = smoothed_spectrum;
    fft_data.history_pos = (fft_data.history_pos + 1) % fft_history_len;
}

fn renderFrame(fft_data: *const FftData, fft_image: *rl.Image) void {
    var frames_since_tapback = @floor(@as(f32, @floatCast(fft_data.tapback_pos / window_time)));
    frames_since_tapback = rl.math.clamp(frames_since_tapback, 0.0, fft_history_len - 1);

    const history_len: i32 = fft_history_len;
    var history_position = @rem(@as(i32, @intCast(fft_data.history_pos)) - 1 - @as(i32, @intFromFloat(frames_since_tapback)), history_len);
    if (history_position < 0) history_position += history_len;

    const amplitude = &fft_data.fft_history[@intCast(history_position)];
    for (amplitude, 0..) |value, bin| {
        const color = rl.colorFromNormalized(.{ .x = value, .y = unused_channel, .z = unused_channel, .w = unused_channel });
        rl.imageDrawPixel(fft_image, @intCast(bin), fft_row, color);
    }
}
/*******************************************************************************************
*
*   raylib [audio] example - spectrum visualizer
*
*   Example complexity rating: [★★★☆] 3/4
*
*   Example originally created with raylib 6.0, last time updated with raylib 6.0
*
*   Inspired by Inigo Quilez's https://www.shadertoy.com/
*   Resources/specification: https://gist.github.com/soulthreads/2efe50da4be1fb5f7ab60ff14ca434b8
*
*   Example created by created by IANN (@meisei4) reviewed by Ramon Santamaria (@raysan5)
*
*   Example licensed under an unmodified zlib/libpng license, which is an OSI-certified,
*   BSD-like license that allows static linking with closed source software
*
*   Copyright (c) 2025 IANN (@meisei4)
*
********************************************************************************************/

#include "raylib.h"

#include "raymath.h"

#include <math.h>
#include <stdlib.h>
#include <string.h>

#if defined(PLATFORM_DESKTOP)
    #define GLSL_VERSION            330
#else   // PLATFORM_ANDROID, PLATFORM_WEB
    #define GLSL_VERSION            100
#endif

#define MONO                           1
#define SAMPLE_RATE                    44100
#define SAMPLE_RATE_F                  44100.0f
#define FFT_WINDOW_SIZE                1024
#define BUFFER_SIZE                    512
#define PER_SAMPLE_BIT_DEPTH           16
#define AUDIO_STREAM_RING_BUFFER_SIZE  (FFT_WINDOW_SIZE*2)
#define EFFECTIVE_SAMPLE_RATE          (SAMPLE_RATE_F*0.5f)
#define WINDOW_TIME                    ((double)FFT_WINDOW_SIZE/(double)EFFECTIVE_SAMPLE_RATE)
#define FFT_HISTORICAL_SMOOTHING_DUR   2.0f
#define MIN_DECIBELS                   (-100.0f) // https://developer.mozilla.org/en-US/docs/Web/API/AnalyserNode/minDecibels
#define MAX_DECIBELS                   (-30.0f)  // https://developer.mozilla.org/en-US/docs/Web/API/AnalyserNode/maxDecibels
#define INVERSE_DECIBEL_RANGE          (1.0f/(MAX_DECIBELS - MIN_DECIBELS))
#define DB_TO_LINEAR_SCALE             (20.0f/2.302585092994046f)
#define SMOOTHING_TIME_CONSTANT        0.8f // https://developer.mozilla.org/en-US/docs/Web/API/AnalyserNode/smoothingTimeConstant
#define TEXTURE_HEIGHT                 1
#define FFT_ROW                        0
#define UNUSED_CHANNEL                 0.0f

typedef struct FFTComplex { float real, imaginary; } FFTComplex;

typedef struct FFTData {
    FFTComplex *spectrum;
    FFTComplex *workBuffer;
    float *prevMagnitudes;
    float (*fftHistory)[BUFFER_SIZE];
    int fftHistoryLen;
    int historyPos;
    double lastFftTime;
    float tapbackPos;
} FFTData;

static void CaptureFrame(FFTData *fftData, const float *audioSamples);
static void RenderFrame(const FFTData *fftData, Image *fftImage);
static void CooleyTukeyFFTSlow(FFTComplex *spectrum, int n);

//------------------------------------------------------------------------------------
// Program main entry point
//------------------------------------------------------------------------------------
int main(void)
{
    // Initialization
    //-----------------------------------------------------------------------------------     ---
    const int screenWidth = 800;
    const int screenHeight = 450;

    InitWindow(screenWidth, screenHeight, "raylib [audio] example - spectrum visualizer");

    Image fftImage = GenImageColor(BUFFER_SIZE, TEXTURE_HEIGHT, WHITE);
    Texture2D fftTexture = LoadTextureFromImage(fftImage);
    RenderTexture2D bufferA = LoadRenderTexture(screenWidth, screenHeight);
    Vector2 iResolution = { (float)screenWidth, (float)screenHeight };

    Shader shader = LoadShader(0, TextFormat("resources/shaders/glsl%i/fft.fs", GLSL_VERSION));

    int iResolutionLocation = GetShaderLocation(shader, "iResolution");
    int iChannel0Location = GetShaderLocation(shader, "iChannel0");
    SetShaderValue(shader, iResolutionLocation, &iResolution, SHADER_UNIFORM_VEC2);
    SetShaderValueTexture(shader, iChannel0Location, fftTexture);

    InitAudioDevice();
    SetAudioStreamBufferSizeDefault(AUDIO_STREAM_RING_BUFFER_SIZE);

    // WARNING: Memory out-of-bounds on PLATFORM_WEB
    Wave wav = LoadWave("resources/country.mp3");
    WaveFormat(&wav, SAMPLE_RATE, PER_SAMPLE_BIT_DEPTH, MONO);

    AudioStream audioStream = LoadAudioStream(SAMPLE_RATE, PER_SAMPLE_BIT_DEPTH, MONO);
    PlayAudioStream(audioStream);

    int fftHistoryLen = (int)ceilf(FFT_HISTORICAL_SMOOTHING_DUR/WINDOW_TIME) + 1;

    FFTData fft = {
        .spectrum = RL_CALLOC(sizeof(FFTComplex), FFT_WINDOW_SIZE),
        .workBuffer = RL_CALLOC(sizeof(FFTComplex), FFT_WINDOW_SIZE),
        .prevMagnitudes = RL_CALLOC(BUFFER_SIZE, sizeof(float)),
        .fftHistory = RL_CALLOC(fftHistoryLen, sizeof(float[BUFFER_SIZE])),
        .fftHistoryLen = fftHistoryLen,
        .historyPos = 0,
        .lastFftTime = 0.0,
        .tapbackPos = 0.01f
    };

    unsigned int wavCursor = 0;
    const short *wavPCM16 = wav.data;

    short chunkSamples[AUDIO_STREAM_RING_BUFFER_SIZE] = { 0 };
    float audioSamples[FFT_WINDOW_SIZE] = { 0 };

    SetTargetFPS(60);
    //----------------------------------------------------------------------------------

    // Main game loop
    while (!WindowShouldClose())    // Detect window close button or ESC key
    {
        // Update
        //----------------------------------------------------------------------------------
        while (IsAudioStreamProcessed(audioStream))
        {
            for (int i = 0; i < AUDIO_STREAM_RING_BUFFER_SIZE; i++)
            {
                int left = (wav.channels == 2)? wavPCM16[wavCursor*2 + 0] : wavPCM16[wavCursor];
                int right = (wav.channels == 2)? wavPCM16[wavCursor*2 + 1] : left;
                chunkSamples[i] = (short)((left + right)/2);

                if (++wavCursor >= wav.frameCount) wavCursor = 0;
            }

            UpdateAudioStream(audioStream, chunkSamples, AUDIO_STREAM_RING_BUFFER_SIZE);

            for (int i = 0; i < FFT_WINDOW_SIZE; i++) audioSamples[i] = (chunkSamples[i*2] + chunkSamples[i*2 + 1])*0.5f/32767.0f;
        }

        CaptureFrame(&fft, audioSamples);
        RenderFrame(&fft, &fftImage);
        UpdateTexture(fftTexture, fftImage.data);
        //----------------------------------------------------------------------------------

        // Draw
        //----------------------------------------------------------------------------------
        BeginDrawing();

            ClearBackground(RAYWHITE);

            BeginShaderMode(shader);
                SetShaderValueTexture(shader, iChannel0Location, fftTexture);
                DrawTextureRec(bufferA.texture,
                    (Rectangle){ 0, 0, (float)screenWidth, (float)-screenHeight },
                    (Vector2){ 0, 0 }, WHITE);
            EndShaderMode();

        EndDrawing();
        //------------------------------------------------------------------------------
    }

    // De-Initialization
    //--------------------------------------------------------------------------------------
    UnloadShader(shader);
    UnloadRenderTexture(bufferA);
    UnloadTexture(fftTexture);
    UnloadImage(fftImage);
    UnloadAudioStream(audioStream);
    UnloadWave(wav);
    CloseAudioDevice();

    RL_FREE(fft.spectrum);
    RL_FREE(fft.workBuffer);
    RL_FREE(fft.prevMagnitudes);
    RL_FREE(fft.fftHistory);

    CloseWindow();        // Close window and OpenGL context
    //----------------------------------------------------------------------------------

    return 0;
}

// Cooley–Tukey FFT https://en.wikipedia.org/wiki/Cooley%E2%80%93Tukey_FFT_algorithm#Data_reordering,_bit_reversal,_and_in-place_algorithms
static void CooleyTukeyFFTSlow(FFTComplex *spectrum, int n)
{
    int j = 0;
    for (int i = 1; i < n - 1; i++)
    {
        int bit = n >> 1;
        while (j >= bit)
        {
            j -= bit;
            bit >>= 1;
        }
        j += bit;
        if (i < j)
        {
            FFTComplex temp = spectrum[i];
            spectrum[i] = spectrum[j];
            spectrum[j] = temp;
        }
    }

    for (int len = 2; len <= n; len <<= 1)
    {
        float angle = -2.0f*PI/len;
        FFTComplex twiddleUnit = { cosf(angle), sinf(angle) };
        for (int i = 0; i < n; i += len)
        {
            FFTComplex twiddleCurrent = { 1.0f, 0.0f };
            for (int j = 0; j < len/2; j++)
            {
                FFTComplex even = spectrum[i + j];
                FFTComplex odd = spectrum[i + j + len/2];
                FFTComplex twiddledOdd = {
                    odd.real*twiddleCurrent.real - odd.imaginary*twiddleCurrent.imaginary,
                    odd.real*twiddleCurrent.imaginary + odd.imaginary*twiddleCurrent.real
                };

                spectrum[i + j].real = even.real + twiddledOdd.real;
                spectrum[i + j].imaginary = even.imaginary + twiddledOdd.imaginary;
                spectrum[i + j + len/2].real = even.real - twiddledOdd.real;
                spectrum[i + j + len/2].imaginary = even.imaginary - twiddledOdd.imaginary;

                float twiddleRealNext = twiddleCurrent.real*twiddleUnit.real - twiddleCurrent.imaginary*twiddleUnit.imaginary;
                twiddleCurrent.imaginary = twiddleCurrent.real*twiddleUnit.imaginary + twiddleCurrent.imaginary*twiddleUnit.real;
                twiddleCurrent.real = twiddleRealNext;
            }
        }
    }
}

static void CaptureFrame(FFTData *fftData, const float *audioSamples)
{
    for (int i = 0; i < FFT_WINDOW_SIZE; i++)
    {
        float x = (2.0f*PI*i)/(FFT_WINDOW_SIZE - 1.0f);
        float blackmanWeight  = 0.42f - 0.5f*cosf(x) + 0.08f*cosf(2.0f*x); // https://en.wikipedia.org/wiki/Window_function#Blackman_window
        fftData->workBuffer[i].real = audioSamples[i]*blackmanWeight;
        fftData->workBuffer[i].imaginary = 0.0f;
    }

    CooleyTukeyFFTSlow(fftData->workBuffer, FFT_WINDOW_SIZE);
    memcpy(fftData->spectrum, fftData->workBuffer, sizeof(FFTComplex)*FFT_WINDOW_SIZE);

    float smoothedSpectrum[BUFFER_SIZE];

    for (int bin = 0; bin < BUFFER_SIZE; bin++)
    {
        float re = fftData->workBuffer[bin].real;
        float im = fftData->workBuffer[bin].imaginary;
        float linearMagnitude = sqrtf(re*re + im*im)/FFT_WINDOW_SIZE;

        float smoothedMagnitude = SMOOTHING_TIME_CONSTANT*fftData->prevMagnitudes[bin] + (1.0f - SMOOTHING_TIME_CONSTANT)*linearMagnitude;
        fftData->prevMagnitudes[bin] = smoothedMagnitude;

        float db = logf(fmaxf(smoothedMagnitude, 1e-40f))*DB_TO_LINEAR_SCALE;
        float normalized = (db - MIN_DECIBELS)*INVERSE_DECIBEL_RANGE;
        smoothedSpectrum[bin] = Clamp(normalized, 0.0f, 1.0f);
    }

    fftData->lastFftTime = GetTime();
    memcpy(fftData->fftHistory[fftData->historyPos], smoothedSpectrum, sizeof(smoothedSpectrum));
    fftData->historyPos = (fftData->historyPos + 1)%fftData->fftHistoryLen;
}

static void RenderFrame(const FFTData *fftData, Image *fftImage)
{
    float framesSinceTapback = floorf((float)(fftData->tapbackPos/WINDOW_TIME));
    framesSinceTapback = Clamp(framesSinceTapback, 0.0f, (float)(fftData->fftHistoryLen - 1));

    int historyPosition = (fftData->historyPos - 1 - (int)framesSinceTapback)%fftData->fftHistoryLen;
    if (historyPosition < 0) historyPosition += fftData->fftHistoryLen;

    const float *amplitude = fftData->fftHistory[historyPosition];
    for (int bin = 0; bin < BUFFER_SIZE; bin++) ImageDrawPixel(fftImage, bin, FFT_ROW, ColorFromNormalized((Vector4){ amplitude[bin], UNUSED_CHANNEL, UNUSED_CHANNEL, UNUSED_CHANNEL }));
}

From raylib 6.2. The original on GitHub.