Skip to content

Instantly share code, notes, and snippets.

@wavefnx
Created March 20, 2025 21:59
Audio controlled shader using Odin and miniaudio
// MIT License | Copyright (c) 2025 wavefnx
//
// # Description
// Use miniaudio to capture microphone audio and use it to dynamically change shader parameters.
//
// This is a compact version of the original (quoted) post: https://x.com/wavefnx/status/1848025294478643454
//
// # Get started
// This gist contains both main.odin and shader.fs files.
// To run it, create a folder, copy main.odin to /main.odin
// and the shader.fs located below to /resources/shader.fs or a selected SHADER_PATH.
//
// Finally, while in the root of the project folder, run the program by executing the command:
// odin run main.odin -file
//
// When the window opens, the mic audio will control the shader's parameters.
//
// # Notice
// - This program has been tested on Arch linux.
// - Verify that the capture device is unmuted and able to capture audio
// -------------------------------------------------------------
// main.odin
// -------------------------------------------------------------
package main
import "base:runtime"
import "core:fmt"
import "core:math"
import "core:os"
import ma "vendor:miniaudio"
import rl "vendor:raylib"
// Higher MAX_SAMPLES values will result in smoother visuals
// but will also increase latency.
MAX_SAMPLES :: 512
WINDOW_WIDTH, WINDOW_HEIGHT :: 800, 600
GUI_TEXT_SIZE :: 22
GUI_TEXT_COLOR :: rl.WHITE
SHADER_PATH :: "resources/shader.fs"
AudioData :: struct {
// Buffer to store audio samples
samples: [MAX_SAMPLES]f32,
// Number of samples currently in the buffer
sample_count: int,
}
AudioAnalysis :: struct {
// Amplitude RMS
rms: f32,
// Peak amplitude
peak: f32,
}
// Global variables
audio_data: AudioData
audio_analysis: AudioAnalysis
// data_callback is the audio capture callback function.
// It is invoked by miniaudio when new audio data is available from the input device.
data_callback :: proc "c" (device: ^ma.device, output: rawptr, input: rawptr, frame_count: u32) {
context = runtime.default_context()
samples := cast([^]f32)input
// Determine number of samples to copy, limited by our maximum buffer size
sample_count := min(int(frame_count), MAX_SAMPLES)
// Copy input samples to our internal audio buffer
copy(audio_data.samples[:sample_count], samples[:sample_count])
// Update the current sample count in our audio data structure
audio_data.sample_count = sample_count
}
// Analyze captured the audio data
analyze_audio :: proc() {
// Reset the analysis
audio_analysis = {}
for i in 0 ..< audio_data.sample_count {
sample := audio_data.samples[i]
audio_analysis.rms += sample * sample
audio_analysis.peak = max(audio_analysis.peak, abs(sample))
}
if audio_data.sample_count > 0 {
audio_analysis.rms = math.sqrt(audio_analysis.rms / f32(audio_data.sample_count))
}
}
main :: proc() {
// Initialize audio device in capture mode (mic)
device_config := ma.device_config_init(ma.device_type.capture)
device_config.capture.format = ma.format.f32
device_config.dataCallback = data_callback
// You can additionally configure more parameters e.g.
// device_config.capture.channels = 2
// device_config.sampleRate = 44100
device: ma.device
result := ma.device_init(nil, &device_config, &device)
if result != .SUCCESS {
fmt.eprintln("failed to initialize audio device:", result)
os.exit(1)
}
defer ma.device_uninit(&device)
result = ma.device_start(&device)
if result != .SUCCESS {
fmt.eprintln("failed to start audio device:", result)
os.exit(1)
}
defer ma.device_stop(&device)
// Initialize Raylib
rl.InitWindow(i32(WINDOW_WIDTH), i32(WINDOW_HEIGHT), "audio shaders")
defer rl.CloseWindow()
rl.SetTargetFPS(60)
// Load and compile the shader
shader := rl.LoadShader(nil, SHADER_PATH)
defer rl.UnloadShader(shader)
// Get shader uniform locations
res_loc := rl.GetShaderLocation(shader, "iResolution")
time_loc := rl.GetShaderLocation(shader, "iTime")
rms_loc := rl.GetShaderLocation(shader, "iRms")
peak_loc := rl.GetShaderLocation(shader, "iPeak")
// Set iResolution uniform (only needs to be set once if window size doesn't change)
resolution := [3]f32{f32(WINDOW_WIDTH), f32(WINDOW_HEIGHT), 0}
rl.SetShaderValue(shader, res_loc, &resolution, .VEC3)
// Main loop
for !rl.WindowShouldClose() {
// Analyze audio
analyze_audio()
// Update shader uniforms
time := f32(rl.GetTime())
rl.SetShaderValue(shader, time_loc, &time, .FLOAT)
rl.SetShaderValue(shader, rms_loc, &audio_analysis.rms, .FLOAT)
rl.SetShaderValue(shader, peak_loc, &audio_analysis.peak, .FLOAT)
// Begin drawing
rl.BeginDrawing()
rl.ClearBackground(rl.BLACK)
rl.BeginShaderMode(shader)
// provide a surface for the shader to draw on
rl.DrawRectangle(0, 0, i32(WINDOW_WIDTH), i32(WINDOW_HEIGHT), rl.WHITE)
rl.EndShaderMode()
// Draw debug info
rl.DrawText(rl.TextFormat("RMS: %.4f", audio_analysis.rms), 10, 10, GUI_TEXT_SIZE, GUI_TEXT_COLOR)
rl.DrawText(rl.TextFormat("Peak: %.4f", audio_analysis.peak), 10, 40, GUI_TEXT_SIZE, GUI_TEXT_COLOR)
rl.EndDrawing()
}
}
// -------------------------------------------------------------
// resources/shader.fs
// -------------------------------------------------------------
#version 330 core
// Constants
#define UV_SCALE 1.25
#define IC_TIME_SCALE_X 3.0
#define IC_TIME_SCALE_Y 2.0
#define IC_AMPLITUDE 2.0
#define LIGHT_INTENSITY 0.1
#define CV_AMPLITUDE 0.2
#define CV_TIME_SCALE_Y 0.25
#define CV_TIME_SCALE_Z 0.1
#define CV_TIME_SCALE_X 0.1
#define RMS_COLOR_FACTOR 1.5
#define NOISE_SCALE 0.1
#define NOISE_SEED_X 12.9898
#define NOISE_SEED_Y 78.233
#define NOISE_FACTOR 43758.5453
uniform vec3 iResolution;
uniform float iTime;
uniform float iRms;
uniform float iPeak;
float icOffsetY = sin(iTime * IC_TIME_SCALE_Y) * iPeak * IC_AMPLITUDE;
float icOffsetX = cos(iTime * IC_TIME_SCALE_X) * iRms * IC_AMPLITUDE;
float cvOffsetX = sin(iTime * CV_TIME_SCALE_X) * CV_AMPLITUDE;
float cvOffsetY = sin(iTime * CV_TIME_SCALE_Y) * CV_AMPLITUDE;
float cvOffsetZ = sin(iTime * CV_TIME_SCALE_Z) * CV_AMPLITUDE;
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = UV_SCALE * (2.0 * fragCoord.xy - iResolution.xy) / iResolution.y;
// Make the inner circle move based on RMS and Peak
vec2 innerCircleOffset = vec2(icOffsetX, icOffsetY);
// Calculate the light color based on the peak value
vec3 lightColor = vec3(0.5 * iPeak, 0.25 * iPeak, 0.25 * iPeak);
// Calculate the light intensity based on the distance from the center of the screen
float light = LIGHT_INTENSITY / distance(normalize(uv), uv);
// Add color variation controlled by time
vec3 color = lightColor + vec3(cvOffsetX, cvOffsetZ, cvOffsetY);
fragColor = vec4(light * color, 1.0);
// Modify the color based on audio input
fragColor.rgb *= (1.0 + iRms * RMS_COLOR_FACTOR);
// Make alpha reactive to audio
fragColor.a = clamp(fragColor.a * (1.0 + iRms + iPeak), 0.0, 1.0);
// Add some subtle noise
fragColor.rgb += (fract(sin(dot(uv, vec2(NOISE_SEED_X, NOISE_SEED_Y))) * NOISE_FACTOR) - 0.5) * NOISE_SCALE;
}
void main()
{
vec4 fragColor;
mainImage(fragColor, gl_FragCoord.xy);
gl_FragColor = fragColor;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment