Introducción al Desarrollo de Videojuegos con GameMaker · Module 9: Music, Sound Effects, and Particles

Volume Control and Audio Settings

Lesson 45 of 60 · 50 min

Also available in: ES

Volume Control and Audio Settings in GameMaker

In previous lessons, you learned how to import and trigger sound effects using functions such as audio_play_sound, structure music loops, and generate dynamic particles to polish the retro feel of our 2D platformer. However, commercial-grade game audio requires more than simply triggering tracks: it demands giving the end user complete control over acoustic intensity. Displays, headphones, and player hearing preferences vary widely, and the lack of in-game volume settings is consistently one of the most cited criticisms in playtesting and user reviews.

The goal of this lesson is to equip our project with a robust, modular, and customizable audio architecture. We will explore GameMaker's audio hierarchy, learn to manipulate overall gain (master volume), and separate our mix between background music and sound effects. Finally, we will connect this logic to interactive visual elements within the Draw GUI event, enabling players to tweak volume levels smoothly using intuitive sliders and selectors.

The Concept of Gain and Mixing Hierarchy

In software development and digital audio engineering, the technical term for the relative volume of an audio signal is "gain". In GameMaker, gain is neither measured in decibels nor expressed as a direct 0 to 100 percentage. Instead, it is represented as a floating-point value typically ranging from 0.0 to 1.0:

Gain Value (Float)Percentage EquivalentPerceptual Effect in Engine
0.00%Absolute silence (Mute). The sound plays internally but produces no audible output signal.
0.550%Half the perceived intensity of the digitized original audio asset.
1.0100%Unity gain (nominal level). The sound plays at the exact standard level at which the asset was exported.
> 1.0> 100%Software amplification. May cause digital distortion (clipping) if the waveform exceeds the maximum allowed amplitude.

To maintain a balanced mix in a commercial video game, audio files are never adjusted in isolation without a structured framework. Instead, we establish a mixing hierarchy that processes the audio signal through distinct stages before routing it to the user's physical speakers.

Audio signal routing diagram showing music and sound effects channels flowing into independent gain controls, then combining into master volume before physical output.
Figure 1: Audio routing and processing hierarchy in GameMaker. · AI-generated image

As the diagram illustrates, the final volume perceived by the player is the product of multiplying consecutive gain values. If a jump sound (`snd_salto`) plays at 0.8 gain, the SFX channel is set to 0.5, and the overall master volume is set to 0.5, the final effective gain for that jump will be 0.8 * 0.5 * 0.5 = 0.2 (20% of its nominal volume). Understanding this cascading multiplication prevents common clipping issues and accidental muting.

Native Gain Control Functions in GML

GameMaker provides a dedicated suite of built-in functions to control each tier of this hierarchy. Below are the core tools we will use to build our system:

1. **Master Control:** The `audio_master_gain(gain)` function defines the global volume ceiling for the entire application. It accepts a real value between 0.0 and 1.0. Updating this function immediately and proportionally scales every sound produced by GameMaker.

2. **Asset or Instance Control:** The `audio_sound_gain(index, gain, time)` function allows you to change the volume of a specific sound asset (`snd_salto`) or an active sound instance (the unique numerical sound ID returned by `audio_play_sound`). Its third argument, `time`, specifies an interpolation duration in milliseconds, making smooth fades possible without abrupt volume jumps.

3. **Audio Groups:** In intermediate and advanced projects, GameMaker allows grouping sound assets in the Asset Browser under discrete Audio Groups (e.g., `audiogroup_default`, `audiogroup_music`). Using `audio_group_set_gain(group, gain, time_ms)`, you can adjust the volume of all sounds assigned to that group simultaneously with a single line of code.

💡 Technical tip: While Audio Groups are best practice for large-scale games with disk streaming, in this lesson we will implement a hybrid controller based on global variables and instance-level gain controls. This ensures full educational clarity without requiring complex IDE asset preloading steps for learners taking their first steps.

Designing the Manager Object: obj_controlador_audio

To ensure player audio settings persist across different rooms in our platformer (from the title screen to deep cavern levels), we will build a persistent manager object. This object will centralize channel states and serve as the single point of entry for any audio requests in the game.

Follow these steps in your project:

1. In the **Asset Browser**, right-click the **Objects** group and select **Create -> Object**. 2. Name the object exactly `obj_controlador_audio`. 3. Check the **Persistent** checkbox. This ensures the object will not be destroyed when transitioning between rooms with `room_goto()`.

Next, let's initialize our configuration variables in the **Create** event of `obj_controlador_audio`. We will use global variables so any other object in the game (such as the player character jumping or an enemy taking damage) can easily query current audio levels if necessary.

// Create Event of obj_controlador_audio

// 1. Prevent duplicates if returning to the starting room
if (instance_number(obj_controlador_audio) > 1) {
    instance_destroy();
    exit;
}

// 2. Master gain variables (range 0.0 to 1.0)
global.volumen_maestro = 0.8;
global.volumen_musica  = 0.7;
global.volumen_sfx     = 1.0;

// Variable to track the currently playing background music
global.musica_actual_instancia = noone;
global.musica_actual_asset     = noone;

// Apply the master gain immediately to the engine
audio_master_gain(global.volumen_maestro);

// UI variables for on-screen slider interaction
slider_seleccionado = -1; // -1: none, 0: master, 1: music, 2: sfx

// Dimensions and margins for visual GUI controls
gui_slider_ancho = 200;
gui_slider_alto  = 16;

Handling duplicates with `instance_number(obj_controlador_audio) > 1` is an essential design pattern. When an object is persistent, returning to a room where the object was originally placed in the Room Editor would cause GameMaker to instantiate a duplicate copy. Destroying secondary instances immediately prevents duplicate audio triggers and memory leaks.

Creating Safe, Modular Playback Functions

To ensure music and sound effect volumes consistently respect the variables we just declared, we should avoid calling raw `audio_play_sound` functions with arbitrary values across multiple objects. A reliable software design pattern is to wrap playback logic inside dedicated script functions that automatically apply the correct gain at the exact moment of playback.

Create a new Script in the Asset Browser and name it `scr_audio_sistema`. Add the following modular code inside:

/// @function reproducir_sfx(sound_asset, prioridad)
/// @description Plays a sound effect applying global SFX gain.
/// @param {Asset.GMSound} _asset Sound asset identifier to play
/// @param {Real} _prioridad Playback priority (e.g., 1 to 10)
function reproducir_sfx(_asset, _prioridad) {
    var _instancia = audio_play_sound(_asset, _prioridad, false);
    
    // Apply the calculated gain immediately to the newly created instance
    audio_sound_gain(_instancia, global.volumen_sfx, 0);
    
    return _instancia;
}

/// @function reproducir_musica(sound_asset, tiempo_fade_ms)
/// @description Handles background music changes with fade support.
/// @param {Asset.GMSound} _asset Music asset to loop
/// @param {Real} _tiempo_fade_ms Transition fade time in milliseconds
function reproducir_musica(_asset, _tiempo_fade_ms) {
    // If requested track is already playing, do nothing
    if (global.musica_actual_asset == _asset && audio_is_playing(global.musica_actual_instancia)) {
        return;
    }
    
    // If previous music is playing, fade it out
    if (audio_is_playing(global.musica_actual_instancia)) {
        audio_sound_gain(global.musica_actual_instancia, 0, _tiempo_fade_ms);
        // Note: The engine will attenuate the track gradually
    }
    
    // Start new track at volume 0 for a smooth fade-in
    global.musica_actual_asset = _asset;
    global.musica_actual_instancia = audio_play_sound(_asset, 100, true);
    audio_sound_gain(global.musica_actual_instancia, 0, 0);
    
    // Transition to configured music volume
    audio_sound_gain(global.musica_actual_instancia, global.volumen_musica, _tiempo_fade_ms);
}

/// @function actualizar_volumenes_en_tiempo_real()
/// @description Updates master gain and active looping audio.
function actualizar_volumenes_en_tiempo_real() {
    // 1. Update engine-wide master gain
    audio_master_gain(global.volumen_maestro);
    
    // 2. If background music is looping, update its gain immediately
    if (audio_is_playing(global.musica_actual_instancia)) {
        audio_sound_gain(global.musica_actual_instancia, global.volumen_musica, 0);
    }
}

With these wrapper functions in place, updating audio calls across your codebase becomes clean and safe:

• Instead of writing `audio_play_sound(snd_salto, 1, false);` in the player object, call `reproducir_sfx(snd_salto, 1);`. • When collecting a coin or defeating an enemy, use `reproducir_sfx(snd_moneda, 2);`. • When loading a level or game room, set the stage with `reproducir_musica(snd_musica_nivel_1, 1000);` for a clean one-second fade-in.

Building the UI in Draw GUI

To make audio settings accessible to the player, we need an interface that visualizes all three channels (Master, Music, and SFX) and lets users adjust them using either a mouse or keyboard controls. As covered in Module 8, the **Draw GUI** event renders elements statically in screen coordinates, unaffected by camera scrolling or room dimensions.

We will render interactive sliders using GameMaker's vector primitives (`draw_rectangle`, `draw_set_color`) without needing dedicated sprite assets right away. This allows us to rapidly prototype and verify our math and logic.

Open `obj_controlador_audio` and add the **Draw GUI** event:

// Draw GUI Event of obj_controlador_audio

// Base coordinates on screen
var _base_x = 100;
var _base_y = 120;
var _espaciado_y = 60;

// Text appearance settings
draw_set_font(-1); // Use default font
draw_set_halign(fa_left);
draw_set_valign(fa_middle);

// Audio settings panel header
draw_set_color(c_white);
draw_text(_base_x, _base_y - 40, "AUDIO SETTINGS (Click & drag with mouse):");

// Temporary data structure to process all three channels in a clean loop
var _canales = [
    { nombre: "Master Volume: ", valor: global.volumen_maestro },
    { nombre: "Music (BGM):   ", valor: global.volumen_musica },
    { nombre: "Sound Effects: ", valor: global.volumen_sfx }
];

for (var i = 0; i < array_length(_canales); i++) {
    var _y_pos = _base_y + (i * _espaciado_y);
    var _nombre = _canales[i].nombre;
    var _val = _canales[i].valor;
    var _porcentaje_texto = string(round(_val * 100)) + "%";
    
    // 1. Draw channel label and percentage value
    draw_set_color(c_yellow);
    draw_text(_base_x, _y_pos - 15, _nombre + _porcentaje_texto);
    
    // 2. Draw slider track background (Dark Gray)
    draw_set_color(c_dkgray);
    draw_rectangle(_base_x, _y_pos, _base_x + gui_slider_ancho, _y_pos + gui_slider_alto, false);
    
    // 3. Draw filled bar based on current volume (Cyan)
    var _ancho_relleno = gui_slider_ancho * _val;
    draw_set_color(c_aqua);
    draw_rectangle(_base_x, _y_pos, _base_x + _ancho_relleno, _y_pos + gui_slider_alto, false);
    
    // 4. Draw outer border of slider track (White)
    draw_set_color(c_white);
    draw_rectangle(_base_x, _y_pos, _base_x + gui_slider_ancho, _y_pos + gui_slider_alto, true);
    
    // 5. Draw slider thumb knob
    var _tirador_x = _base_x + _ancho_relleno;
    draw_set_color(c_white);
    draw_rectangle(_tirador_x - 3, _y_pos - 4, _tirador_x + 3, _y_pos + gui_slider_alto + 4, false);
}

// Reset default drawing color
draw_set_color(c_white);

Detecting Cursor Interaction (Step Event)

With the three bars and their numeric labels rendered on screen, we need to make them interactive. When the player clicks on a slider bar and drags the mouse horizontally, we will calculate the relative cursor position along the slider width to set a normalized gain value (between 0.0 and 1.0).

Because we are interacting with elements drawn in the GUI layer, we must avoid using `mouse_x` and `mouse_y` (which represent world coordinates inside the room). Instead, we must use `device_mouse_x_to_gui(0)` and `device_mouse_y_to_gui(0)`.

Add the following code into the **Step** event of `obj_controlador_audio`:

// Step Event of obj_controlador_audio

// Retrieve cursor coordinates relative to the GUI layer
var _gui_mouse_x = device_mouse_x_to_gui(0);
var _gui_mouse_y = device_mouse_y_to_gui(0);

var _base_x = 100;
var _base_y = 120;
var _espaciado_y = 60;

// 1. Detect when left mouse button is pressed (start dragging)
if (mouse_check_button_pressed(mb_left)) {
    for (var i = 0; i < 3; i++) {
        var _y_pos = _base_y + (i * _espaciado_y);
        
        // Check collision between cursor and interactive slider area
        if (point_in_rectangle(_gui_mouse_x, _gui_mouse_y, _base_x, _y_pos - 4, _base_x + gui_slider_ancho, _y_pos + gui_slider_alto + 4)) {
            slider_seleccionado = i;
            break;
        }
    }
}

// 2. Stop dragging when left mouse button is released
if (mouse_check_button_released(mb_left)) {
    // If releasing SFX slider, play a sample sound to confirm volume level
    if (slider_seleccionado == 2) {
        reproducir_sfx(snd_moneda, 10);
    }
    slider_seleccionado = -1;
}

// 3. Update volume in real-time while dragging
if (slider_seleccionado != -1 && mouse_check_button(mb_left)) {
    // Normalize mouse position within [0.0, 1.0] range
    var _nuevo_volumen = (_gui_mouse_x - _base_x) / gui_slider_ancho;
    
    // Clamp the value to ensure it stays within bounds
    _nuevo_volumen = clamp(_nuevo_volumen, 0.0, 1.0);
    
    // Apply new value to selected channel
    switch (slider_seleccionado) {
        case 0:
            global.volumen_maestro = _nuevo_volumen;
            break;
        case 1:
            global.volumen_musica = _nuevo_volumen;
            break;
        case 2:
            global.volumen_sfx = _nuevo_volumen;
            break;
    }
    
    // Apply changes across active audio systems
    actualizar_volumenes_en_tiempo_real();
}

Let's review the logic implemented above:

• `point_in_rectangle(px, py, x1, y1, x2, y2)` efficiently evaluates whether the cursor is positioned within the hit box of any of the three sliders. • `(_gui_mouse_x - _base_x) / gui_slider_ancho` is the normalization formula: when the cursor is at the left edge (`_base_x`), the result is 0.0. When dragged to the far right (`_base_x + gui_slider_ancho`), the result equals 1.0. • `clamp(value, min, max)` safeguards the system against overflow. Even if the player clicks and drags rapidly past the window margins, the volume will never drop below 0.0 or exceed 1.0.

Keyboard Control: Accessibility for Keys and Gamepads

In a retro platformer played with a keyboard or gamepad, a mouse is not always readily accessible—especially when players navigate pause menus in the heat of action. Let's expand our audio manager to allow quick volume adjustments using dedicated keys.

Saving Settings to Disk Using INI Files

Player customization is frustrating if settings reset to defaults every time the game is closed and relaunched. While Modules 10 and 12 explore complete save systems and persistent game data, GameMaker provides a lightweight, native configuration system through `.ini` files that is ideal for audio preferences.

To implement loading and saving, we will add two essential routines to our audio manager.

In the **Create** event of `obj_controlador_audio`, update the initialization block so it reads previously saved preferences from disk:

// At the end of Create Event in obj_controlador_audio:

// Attempt to load saved preferences from disk
ini_open("configuracion_audio.ini");

global.volumen_maestro = ini_read_real("Audio", "master", 0.8);
global.volumen_musica  = ini_read_real("Audio", "musica", 0.7);
global.volumen_sfx     = ini_read_real("Audio", "sfx", 1.0);

ini_close();

// Synchronize audio engine with loaded settings
actualizar_volumenes_en_tiempo_real();

Next, in the **Clean Up** event of the same object (which runs automatically whenever the object is destroyed or the application exits cleanly), save the active values:

// Clean Up Event of obj_controlador_audio

// Save active settings to the local configuration file
ini_open("configuracion_audio.ini");

ini_write_real("Audio", "master", global.volumen_maestro);
ini_write_real("Audio", "musica", global.volumen_musica);
ini_write_real("Audio", "sfx", global.volumen_sfx);

ini_close();

💡 Cross-platform best practice: GameMaker's INI file system is transparent and reliable for lightweight user preferences. It functions identically across Windows, macOS, Linux, HTML5 browsers, and consoles, saving data directly into the isolated local storage directory (Sandbox) allocated for your game.

Testing and Integration in the Retro Project

To verify the integration of everything built throughout this module, let's run a test in our game level room:

1. Open your project's starting room (such as `rm_menu_inicio` or `rm_nivel_1`). 2. Place an instance of `obj_controlador_audio` on a standard instance layer (such as `Instances`). Because it is marked Persistent, a single instance created at startup will endure across the entire game session. 3. Make sure you have a background music call configured at room start: `reproducir_musica(snd_musica_nivel_1, 1500);`. 4. Run the game in test mode (F5). 5. Drag the on-screen sliders using the mouse. You will notice that dragging the music slider to the left reduces background music intensity linearly without stuttering. When jumping or firing, sound effects will maintain their proportionate volume or scale in real time when adjusting the SFX slider.

Practical Exercises

To consolidate the concepts and logic covered in this lesson, complete the following three practical exercises:

1. **Stepped Volume Slider (Discrete Steps):** Rather than continuous floating-point values, modify the slider input reading so the volume snaps into discrete 25% increments (0%, 25%, 50%, 75%, 100%). *Verification criteria:* The numeric percentage displayed in the Draw GUI event must only show multiples of 25, and clicking anywhere along the track must snap the slider knob to one of those five positions.

2. **Master Mute Toggle:** Add key detection for the 'M' key (or an on-screen interactive button in the GUI) to toggle between muted state (master gain at 0.0) and restoring the previous volume level. *Verification criteria:* Pressing 'M' must silence game audio immediately. Pressing 'M' again must restore master volume to its exact pre-mute level without resetting slider positions.

3. **Audio Ducking on Pause:** Connect your audio system with the pause menu built in Module 8. When the game enters the paused state, background music should attenuate to half of its configured volume (`global.volumen_musica * 0.5`) over a 500-millisecond fade, then smoothly restore to its normal volume when gameplay resumes. *Verification criteria:* Pausing softens the music to let the pause menu stand out, and resuming returns the music to its full volume without audible popping or abrupt cut-offs.

Summary

  • Gain in GameMaker is a normalized floating-point value ranging from 0.0 (complete silence) to 1.0 (standard nominal maximum volume).
  • The audio_master_gain function controls the overall audio output level across the entire application.
  • Separating music and sound effects through modular helper functions (reproducir_sfx, reproducir_musica) streamlines acoustic balance and maintenance.
  • The Draw GUI event combined with device_mouse_x_to_gui and device_mouse_y_to_gui provides an exact coordinate space for camera-independent UI sliders.
  • Using clamp ensures user input always stays within safe numerical boundaries for audio hardware.
  • INI file storage (ini_read_real / ini_write_real) allows user audio preferences to persist seamlessly across gameplay sessions.

Practise what you learned

Exercise 1 · 1 point

Match each native GameMaker function to its specific role in audio gain control:

  • audio_master_gain(gain)
  • audio_sound_gain(index, gain, time)
  • audio_group_set_gain(group, gain, time_ms)
You are practising without an account: nothing is saved. Sign up to keep your progress.

Exercise 2 · 1 point

In GameMaker, final playback volume is determined by multiplying gain levels along the hierarchy. If a sound effect (snd_salto) plays with a gain of 0.8, the SFX group/channel is set to 0.5, and the master volume is set to 0.5, what is the actual final gain of that sound? (Enter the result as a decimal number).

You are practising without an account: nothing is saved. Sign up to keep your progress.

Exercise 3 · 2 points

In the audio manager script, complete the reproducir_sfx function using audio_sound_gain to immediately apply (over 0 milliseconds) the gain stored in global.volumen_sfx to the local variable _instancia.

Your code is not executed: specific requirements are checked on the text.

You are practising without an account: nothing is saved. Sign up to keep your progress.

Sign in to save your progress — Sign in