Advertisement
Game Audio

Godot 4 Platformer Jump Sound Tutorial: AudioStreamPlayer, Pitch Randomization, and Jump Buffering

A single AudioStreamPlayer cuts itself off on rapid jumps. max_polyphony or a player pool fixes that. Coyote time moves the execution frame, so the sound has to move with it. Here is the setup.

A jump sound in Godot 4 is easy to wire up and easy to get subtly wrong. The obvious implementation — one AudioStreamPlayer with the jump clip assigned, calling play() inside the jump condition — works in a prototype and fails under three common conditions: rapid successive jumps, Coyote Time, and pitch variation that is too wide or applied at the wrong layer.

The sections below work through the three decisions that actually change the result: which playback method survives overlapping jumps, which pitch-randomisation approach produces variation without warping the sound, and how Coyote Time and jump buffering move the moment the sound needs to fire. The code is minimal and focuses on the audio call, not a full character controller.

A Single AudioStreamPlayer Cuts Itself Off on Rapid Jumps. max_polyphony Fixes That.

The default AudioStreamPlayer in Godot 4 has a max_polyphony of 1. That means it can play one instance of its stream at a time, and when play() is called again while the first instance is still running, the new call cuts off the previous one. A Godot audio reference states the behaviour directly: “A single player cuts itself off when retriggered” .

For a platformer jump, this produces an audible stutter when the player taps jump twice in quick succession. The first jump sound is cut mid-tail, and the second begins immediately, which reads as one broken sound rather than two jumps. The same reference gives the fix: for overlapping SFX, either raise max_polyphony or spawn a throwaway player that frees itself when finished . Godot’s own documentation confirms that max_polyphony controls how many sounds the node can play at the same time, and that when the limit is exceeded, the oldest sound is cut .

A practical setting for a jump is max_polyphony = 3. That allows up to three overlapping jump sounds, which covers rapid inputs without allowing an unbounded stack. Setting it to a very high number is rarely necessary and can mask a different problem: if the jump is firing far more often than the player is pressing the button, the issue is the trigger condition, not the polyphony limit.

@onready var jump_player: AudioStreamPlayer = $JumpPlayer

func _ready() -> void:
    jump_player.max_polyphony = 3

The alternative is a small pool of players, which is more code but gives finer control over which instance is reused. For a single jump sound, max_polyphony is the simpler correct answer. The sound variation system guide covers pooling at a larger scale; the jump-specific point is that one player with a polyphony limit is sufficient until the game has many simultaneous sound sources.

Pitch Randomisation: randf_range on pitch_scale vs AudioStreamRandomPitch

Godot 4 offers two ways to vary a jump sound’s pitch, and they are not equivalent. The first is to set pitch_scale on the player before calling play(), using a random range. A common pattern from Godot audio guidance is randf_range(0.95, 1.05), described as “slight variation avoids fatigue” . Another example uses randf_range(0.9, 1.1) for a jump .

The second is AudioStreamRandomPitch, an AudioStream resource that wraps another stream and randomly varies the pitch on each start . Its random_pitch property has a default of 1.1, and the pitch is chosen between 1.0 / random_pitch and random_pitch . A value of 1.0 means no variation; a value of 2.0 means the pitch varies between half and double .

Approach Where the variation lives When it is the right choice
pitch_scale = randf_range(...) In code, on the player When you want the variation to be controllable per call, or to combine it with other logic
AudioStreamRandomPitch In the resource, applied automatically on each play When the variation is a fixed property of the asset and you do not want to remember to set it in code

The practical difference is where the randomisation is applied. pitch_scale multiplies the playback rate and also changes the duration, because it is a playback-speed change. AudioStreamRandomPitch is a stream-level wrapper, and its effect is the same kind of pitch shift. Both will shorten the sound as they raise the pitch, which matters for a jump because a shorter tail can be an advantage — it prevents overlap with the landing — but also because a very wide range can make the jump feel inconsistent.

The safe range for a jump is narrow. randf_range(0.95, 1.05) is roughly ±5%, or about ±0.85 semitones, which is below the threshold where the pitch change becomes the dominant percept. randf_range(0.9, 1.1) is roughly ±1.7 semitones, which is still usable for a jump but starts to be noticeable as a different sound rather than a variation. A random_pitch of 1.1 on AudioStreamRandomPitch gives the same ±1.7-semitone range. Values above that tend to make the jump read as unstable rather than varied.

Coyote Time and Jump Buffering Move the Execution Frame. The Sound Has to Move With It.

Coyote Time allows the player to jump for a short window after leaving a ledge. Jump buffering allows a jump press slightly before landing to be remembered and executed on contact. Both are standard platformer forgiveness mechanics, and both change the moment the jump executes relative to the moment the player presses the button.

Common values from Godot 4 platformer implementations are Coyote Time of 0.15 seconds and jump buffering of 0.10 seconds . A frame-perfect implementation may express these as a number of frames instead of seconds, but the principle is the same: the input is remembered for a short window, and the jump executes on the first frame where the condition is satisfied.

@export var coyote_time: float = 0.15
@export var jump_buffer: float = 0.10

var _coyote_timer: float = 0.0
var _jump_buffer_timer: float = 0.0

func _physics_process(delta: float) -> void:
    _coyote_timer -= delta
    _jump_buffer_timer -= delta

    if is_on_floor():
        _coyote_timer = coyote_time

    if Input.is_action_just_pressed("jump"):
        _jump_buffer_timer = jump_buffer

    if _jump_buffer_timer > 0.0 and _coyote_timer > 0.0:
        _perform_jump()

func _perform_jump() -> void:
    velocity.y = -400.0
    _coyote_timer = 0.0
    _jump_buffer_timer = 0.0

    jump_player.pitch_scale = randf_range(0.95, 1.05)
    jump_player.play()

The critical placement is the sound call. If the sound is played when the input is pressed rather than when the jump executes, a buffered jump will play its sound before the character leaves the ground. The player hears a jump, then sees the character still falling for a few frames before the actual jump. That is a different disconnect from a late sound, and it is caused by the same separation between input time and execution time.

The sound should be called inside _perform_jump(), after the velocity is set and the timers are cleared. At that point the jump is committed, and the sound is the confirmation of the committed action, not the input that may or may not become a jump.

The Unity 2D jump setup guide covers the same execution-versus-input distinction in a different engine; the Godot version adds that _physics_process() is the correct loop for both the timer and the sound, because the jump execution is a physics decision.

A Working Godot 4 Setup, in One View

The table below is the audio-relevant configuration, not a full controller. Each row is a decision that changes whether the jump sound fires correctly.

Decision Setting Why
Player node AudioStreamPlayer (non-positional) The jump sound belongs to the player character, not to a world position; positional audio would pan it with the camera
Polyphony max_polyphony = 3 Prevents rapid jumps from cutting off each other; three is enough for a platformer jump
Pitch variation pitch_scale = randf_range(0.95, 1.05), or AudioStreamRandomPitch with random_pitch = 1.1 Breaks exact repetition without making the jump read as a different sound
Sound call Inside _perform_jump(), after velocity is set Ensures the sound fires on execution, not on input, so buffered jumps do not play early
Coyote Time 0.15 s; trigger condition includes _coyote_timer > 0 A Coyote Time jump that succeeds silently is a trigger bug, not a physics bug
Jump buffer 0.10 s; sound fires when the buffer executes A buffered jump that plays its sound on press will sound early relative to the character leaving the ground

A jump sound that fires once per committed jump, with enough polyphony to overlap, a narrow pitch variation, and the call placed inside the execution branch, has no structural reason to feel disconnected. The remaining question — whether the jump clip itself reads as a jump rather than as a blip or a power-up — is a sound-design decision, and the four-parameter jump diagnosis covers the cases where the timing is correct but the sound is still weak.

Advertisement