amp envelope
There is no browser build of this example yet.
Is it the same as C?
- Desktop:verified The Zig version renders a final frame that is byte-identical to the C example.
- Browser:not checked yet The browser comparison does not cover this example yet.
//! raylib [audio] example - amp envelope
//!
//! Zig-API translation of examples/audio/audio_amp_envelope.c (raylib 6.2).
//! Example contributed by Arbinda Rizki Muhammad (@arbipink) and reviewed by Ramon Santamaria (@raysan5)
//! Example licensed under an unmodified zlib/libpng license.
//! Copyright (c) 2026 Arbinda Rizki Muhammad (@arbipink)
const rl = @import("raylib");
const gui = @import("raygui");
const buffer_size = 4096;
const sample_rate = 44100;
/// Calculate the time delta for ONE sample (1/44100)
const sample_time: f32 = 1.0 / @as(f32, sample_rate);
/// Grouping all ADSR parameters and state into a struct
const Envelope = struct {
attack_time: f32,
decay_time: f32,
sustain_level: f32,
release_time: f32,
current_value: f32 = 0.0,
state: State = .idle,
/// Wave state
const State = enum { idle, attack, decay, sustain, release };
/// Advance the envelope by one sample.
fn update(env: *Envelope) void {
switch (env.state) {
.attack => {
env.current_value += (1.0 / env.attack_time) * sample_time;
if (env.current_value >= 1.0) {
env.current_value = 1.0;
env.state = .decay;
}
},
.decay => {
env.current_value -= ((1.0 - env.sustain_level) / env.decay_time) * sample_time;
if (env.current_value <= env.sustain_level) {
env.current_value = env.sustain_level;
env.state = .sustain;
}
},
.sustain => env.current_value = env.sustain_level,
.release => {
env.current_value -= (env.sustain_level / env.release_time) * sample_time;
if (env.current_value <= 0.001) { // Use a small threshold to avoid infinite tail
env.current_value = 0.0;
env.state = .idle;
}
},
.idle => {},
}
}
/// Draw the envelope shape inside `bounds`.
fn drawGraph(env: Envelope, bounds: rl.Rectangle) void {
rl.drawRectangleRec(bounds, rl.fade(.light_gray, 0.3));
rl.drawRectangleLinesEx(bounds, 1, .gray);
// Fixed visual width for sustain stage since it's an amplitude not a time value
const sustain_width: f32 = 1.0;
// Total time to visualize (sum of A, D, R + a padding for Sustain)
const total_time = env.attack_time + env.decay_time + sustain_width + env.release_time;
const scale_x = bounds.width / total_time;
const scale_y = bounds.height;
const start: rl.Vector2 = .{ .x = bounds.x, .y = bounds.y + bounds.height };
const peak: rl.Vector2 = .{ .x = start.x + (env.attack_time * scale_x), .y = bounds.y };
const sustain: rl.Vector2 = .{ .x = peak.x + (env.decay_time * scale_x), .y = bounds.y + (1.0 - env.sustain_level) * scale_y };
const rel: rl.Vector2 = .{ .x = sustain.x + (sustain_width * scale_x), .y = sustain.y };
const end: rl.Vector2 = .{ .x = rel.x + (env.release_time * scale_x), .y = bounds.y + bounds.height };
rl.drawLineV(start, peak, .sky_blue);
rl.drawLineV(peak, sustain, .blue);
rl.drawLineV(sustain, rel, .dark_blue);
rl.drawLineV(rel, end, .orange);
rl.drawText("ADSR Visualizer", @intFromFloat(bounds.x), @intFromFloat(bounds.y - 20), 10, .dark_gray);
}
};
/// Sine wave sample at `audio_time`, scaled by the envelope; advances the phase.
fn sineSample(envelope_value: f32, audio_time: *f32) f32 {
const frequency = 440;
const sample = envelope_value * @sin(2.0 * rl.pi * @as(f32, frequency) * audio_time.*);
audio_time.* += 1.0 / @as(f32, sample_rate);
return sample;
}
pub fn main() !void {
// Initialization
const screen_width = 800;
const screen_height = 450;
rl.initWindow(screen_width, screen_height, "raylib [audio] example - amp envelope");
defer rl.closeWindow();
rl.initAudioDevice();
defer rl.closeAudioDevice();
// Set the number of samples the stream will keep in memory at a time to buffer_size
rl.setAudioStreamBufferSizeDefault(buffer_size);
var buffer: [buffer_size]f32 = @splat(0);
// Init raw audio stream (sample rate: 44100, sample size: 32bit-float, channels: 1-mono)
const stream = try rl.loadAudioStream(sample_rate, 32, 1);
defer rl.unloadAudioStream(stream);
// Init Phase
var audio_time: f32 = 0.0;
var env: Envelope = .{
.attack_time = 1.0,
.decay_time = 1.0,
.sustain_level = 0.5,
.release_time = 1.0,
};
rl.setTargetFPS(60);
// Main game loop
while (!rl.windowShouldClose()) {
// Update
if (rl.isKeyPressed(.space)) env.state = .attack;
if (rl.isKeyReleased(.space) and env.state != .idle) env.state = .release;
if (rl.isAudioStreamProcessed(stream)) {
if (env.state != .idle or env.current_value > 0.0) {
for (&buffer) |*sample| {
env.update();
sample.* = sineSample(env.current_value, &audio_time);
}
} else {
// Clear buffer if silent to avoid looping noise
@memset(&buffer, 0);
audio_time = 0.0;
}
rl.updateAudioStream(stream, &buffer, buffer_size);
}
if (!rl.isAudioStreamPlaying(stream)) rl.playAudioStream(stream);
// Draw
rl.beginDrawing();
defer rl.endDrawing();
rl.clearBackground(.ray_white);
_ = gui.sliderBar(.{ .x = 100, .y = 60, .width = 400, .height = 30 }, "Attack (s)", rl.textFormat("{d:.2}s", .{env.attack_time}), &env.attack_time, 0.1, 3.0);
_ = gui.sliderBar(.{ .x = 100, .y = 100, .width = 400, .height = 30 }, "Decay (s)", rl.textFormat("{d:.2}s", .{env.decay_time}), &env.decay_time, 0.1, 3.0);
_ = gui.sliderBar(.{ .x = 100, .y = 140, .width = 400, .height = 30 }, "Sustain", rl.textFormat("{d:.2}", .{env.sustain_level}), &env.sustain_level, 0.0, 1.0);
_ = gui.sliderBar(.{ .x = 100, .y = 180, .width = 400, .height = 30 }, "Release (s)", rl.textFormat("{d:.2}s", .{env.release_time}), &env.release_time, 0.1, 3.0);
env.drawGraph(.{ .x = 100, .y = 250, .width = 400, .height = 100 });
rl.drawCircleV(.{ .x = 520, .y = 350 - (env.current_value * 100) }, 5, .maroon);
rl.drawText(rl.textFormat("Current Gain: {d:.2}", .{env.current_value}), 535, @intFromFloat(345 - (env.current_value * 100)), 10, .maroon);
rl.drawText("Press SPACE to PLAY the sound!", 200, 400, 20, .light_gray);
}
}/*******************************************************************************************
*
* raylib [audio] example - amp envelope
*
* Example complexity rating: [★☆☆☆] 1/4
*
* Example originally created with raylib 6.0, last time updated with raylib 6.0
*
* Example contributed by Arbinda Rizki Muhammad (@arbipink) and 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) 2026 Arbinda Rizki Muhammad (@arbipink)
*
********************************************************************************************/
#include "raylib.h"
#define RAYGUI_IMPLEMENTATION
#include "raygui.h"
#include <math.h> // Required for: sinf()
#define BUFFER_SIZE 4096
#define SAMPLE_RATE 44100
// Wave state
typedef enum {
IDLE,
ATTACK,
DECAY,
SUSTAIN,
RELEASE
} ADSRState;
// Grouping all ADSR parameters and state into a struct
typedef struct {
float attackTime;
float decayTime;
float sustainLevel;
float releaseTime;
float currentValue;
ADSRState state;
} Envelope;
//------------------------------------------------------------------------------------
// Module Functions Declaration
//------------------------------------------------------------------------------------
static void FillAudioBuffer(int i, float *buffer, float envelopeValue, float *audioTime);
static void UpdateEnvelope(Envelope *env);
static void DrawADSRGraph(Envelope *env, Rectangle bounds);
//------------------------------------------------------------------------------------
// Program main entry point
//------------------------------------------------------------------------------------
int main(void)
{
// Initialization
//--------------------------------------------------------------------------------------
const int screenWidth = 800;
const int screenHeight = 450;
InitWindow(screenWidth, screenHeight, "raylib [audio] example - amp envelope");
InitAudioDevice();
// Set the number of samples the stream will keep in memory at a time to BUFFER_SIZE
SetAudioStreamBufferSizeDefault(BUFFER_SIZE);
float buffer[BUFFER_SIZE] = { 0 };
// Init raw audio stream (sample rate: 44100, sample size: 32bit-float, channels: 1-mono)
AudioStream stream = LoadAudioStream(SAMPLE_RATE, 32, 1);
// Init Phase
float audioTime = 0.0f;
// Initialize the struct
Envelope env = {
.attackTime = 1.0f,
.decayTime = 1.0f,
.sustainLevel = 0.5f,
.releaseTime = 1.0f,
.currentValue = 0.0f,
.state = IDLE
};
SetTargetFPS(60);
//--------------------------------------------------------------------------------------
// Main game loop
while (!WindowShouldClose())
{
// Update
//----------------------------------------------------------------------------------
if (IsKeyPressed(KEY_SPACE)) env.state = ATTACK;
if (IsKeyReleased(KEY_SPACE) && (env.state != IDLE)) env.state = RELEASE;
if (IsAudioStreamProcessed(stream))
{
if ((env.state != IDLE) || (env.currentValue > 0.0f))
{
for (int i = 0; i < BUFFER_SIZE; i++)
{
UpdateEnvelope(&env);
FillAudioBuffer(i, buffer, env.currentValue, &audioTime);
}
}
else
{
// Clear buffer if silent to avoid looping noise
for (int i = 0; i < BUFFER_SIZE; i++) buffer[i] = 0;
audioTime = 0.0f;
}
UpdateAudioStream(stream, buffer, BUFFER_SIZE);
}
if (!IsAudioStreamPlaying(stream)) PlayAudioStream(stream);
//----------------------------------------------------------------------------------
// Draw
//----------------------------------------------------------------------------------
BeginDrawing();
ClearBackground(RAYWHITE);
GuiSliderBar((Rectangle){ 100, 60, 400, 30 }, "Attack (s)", TextFormat("%2.2fs", env.attackTime), &env.attackTime, 0.1f, 3.0f);
GuiSliderBar((Rectangle){ 100, 100, 400, 30 }, "Decay (s)", TextFormat("%2.2fs", env.decayTime), &env.decayTime, 0.1f, 3.0f);
GuiSliderBar((Rectangle){ 100, 140, 400, 30 }, "Sustain", TextFormat("%2.2f", env.sustainLevel), &env.sustainLevel, 0.0f, 1.0f);
GuiSliderBar((Rectangle){ 100, 180, 400, 30 }, "Release (s)", TextFormat("%2.2fs", env.releaseTime), &env.releaseTime, 0.1f, 3.0f);
DrawADSRGraph(&env, (Rectangle){ 100, 250, 400, 100 });
DrawCircleV((Vector2){ 520, 350 - (env.currentValue * 100) }, 5, MAROON);
DrawText(TextFormat("Current Gain: %2.2f", env.currentValue), 535, 345 - (env.currentValue * 100), 10, MAROON);
DrawText("Press SPACE to PLAY the sound!", 200, 400, 20, LIGHTGRAY);
EndDrawing();
//----------------------------------------------------------------------------------
}
// De-Initialization
//--------------------------------------------------------------------------------------
UnloadAudioStream(stream);
CloseAudioDevice();
CloseWindow();
//--------------------------------------------------------------------------------------
return 0;
}
//------------------------------------------------------------------------------------
// Module Functions Definition
//------------------------------------------------------------------------------------
static void FillAudioBuffer(int i, float *buffer, float envelopeValue, float *audioTime)
{
int frequency = 440;
buffer[i] = envelopeValue*sinf(2.0f*PI*frequency*(*audioTime));
*audioTime += (1.0f/SAMPLE_RATE);
}
static void UpdateEnvelope(Envelope *env)
{
// Calculate the time delta for ONE sample (1/44100)
float sampleTime = 1.0f/SAMPLE_RATE;
switch(env->state)
{
case ATTACK:
{
env->currentValue += (1.0f/env->attackTime)*sampleTime;
if (env->currentValue >= 1.0f)
{
env->currentValue = 1.0f;
env->state = DECAY;
}
} break;
case DECAY:
{
env->currentValue -= ((1.0f - env->sustainLevel)/env->decayTime)*sampleTime;
if (env->currentValue <= env->sustainLevel)
{
env->currentValue = env->sustainLevel;
env->state = SUSTAIN;
}
} break;
case SUSTAIN:
{
env->currentValue = env->sustainLevel;
} break;
case RELEASE:
{
env->currentValue -= (env->sustainLevel/env->releaseTime)*sampleTime;
if (env->currentValue <= 0.001f) // Use a small threshold to avoid infinite tail
{
env->currentValue = 0.0f;
env->state = IDLE;
}
} break;
default: break;
}
}
static void DrawADSRGraph(Envelope *env, Rectangle bounds)
{
DrawRectangleRec(bounds, Fade(LIGHTGRAY, 0.3f));
DrawRectangleLinesEx(bounds, 1, GRAY);
// Fixed visual width for sustain stage since it's an amplitude not a time value
float sustainWidth = 1.0f;
// Total time to visualize (sum of A, D, R + a padding for Sustain)
float totalTime = env->attackTime + env->decayTime + sustainWidth + env->releaseTime;
float scaleX = bounds.width/totalTime;
float scaleY = bounds.height;
Vector2 start = { bounds.x, bounds.y + bounds.height };
Vector2 peak = { start.x + (env->attackTime*scaleX), bounds.y };
Vector2 sustain = { peak.x + (env->decayTime*scaleX), bounds.y + (1.0f - env->sustainLevel)*scaleY };
Vector2 rel = { sustain.x + (sustainWidth*scaleX), sustain.y };
Vector2 end = { rel.x + (env->releaseTime*scaleX), bounds.y + bounds.height };
DrawLineV(start, peak, SKYBLUE);
DrawLineV(peak, sustain, BLUE);
DrawLineV(sustain, rel, DARKBLUE);
DrawLineV(rel, end, ORANGE);
DrawText("ADSR Visualizer", bounds.x, bounds.y - 20, 10, DARKGRAY);
}From raylib 6.2. The original on GitHub.