Created
March 20, 2025 21:59
Audio controlled shader using Odin and miniaudio
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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() | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ------------------------------------------------------------- | |
// 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