Lesson 16

Sound Effects

Two ways to make a noise: load a .wav file, or build a beep in code with no file at all.

Loading the game engine. This takes a moment the first time.

This lesson shows TWO ways to get a sound: 1. Load it from a file: pickup.wav, sitting in this lesson's folder. 2. Build it in code: no file at all. See _make_beep() at the bottom.

New code in this lesson

None of the earlier lessons use these. Each one shows the line where it appears here.

stream

The sound an AudioStreamPlayer plays.

game.gd
pickup_sound.stream = pickup_stream

play()

Starts a sound, or an animation by its name, playing.

game.gd
pickup_sound.play()

_input(event)

Godot runs this once for every key press, click or mouse move.

game.gd
func _input(event: InputEvent) -> void:

is_action_pressed()

True when that key is down. On Input it stays true while the key is held; on an event it means this event is the key going down.

game.gd
if event.is_action_pressed("jump"):

AudioStreamWAV

A sound built from numbers in code. mix_rate is how many numbers make one second, and data holds them.

game.gd
func _make_beep(frequency: float, duration: float) -> AudioStreamWAV:

int() and float()

Turns a value into a whole number or a decimal. int() chops off anything after the point.

game.gd
var sample_count: int = int(duration * sample_rate)

PackedByteArray

A list of raw bytes, the smallest numbers a computer stores. resize() sets how many it holds.

game.gd
var data: PackedByteArray = PackedByteArray()

sin()

Turns an angle into a smooth wave between -1 and 1. cos() is the same wave, a quarter turn later.

game.gd
var value: float = sin(t * frequency * TAU) * 0.5

TAU

One full turn in radians, about 6.28.

game.gd
var value: float = sin(t * frequency * TAU) * 0.5

.new()

Makes a new object straight from code, like ColorRect.new(), instead of building it in the editor.

game.gd
var stream: AudioStreamWAV = AudioStreamWAV.new()

The scenes

■ UI node · ■ 2D node · % unique name · .tscn instanced scene

game.tscn
  • Game Node2D game.gd
  • Player CharacterBody2D player.tscn
  • PickupArea Area2D %
  • Polygon2D
  • CollisionShape2D
  • PickupSound AudioStreamPlayer %
  • JumpSound AudioStreamPlayer %
  • HUD CanvasLayer % hud.tscn
hud.tscn
  • HUD CanvasLayer hud.gd
  • MarginContainer
  • MessageLabel Label %
player.tscn
  • Player CharacterBody2D player.gd
  • Sprite2D
  • CollisionShape2D

FileSystem

  • game.tscn
  • game.gd
  • hud.tscn
  • hud.gd
  • player.tscn
  • player.gd

The code

extends Node2D

## The pickup sound, loaded from a file in this lesson's folder.
var pickup_stream: AudioStream = preload("res://lessons/sound_effects/pickup.wav")

@onready var pickup_sound: AudioStreamPlayer = %PickupSound
@onready var jump_sound: AudioStreamPlayer = %JumpSound
@onready var pickup_area: Area2D = %PickupArea
@onready var hud: SoundHUD = %HUD

func _ready() -> void:
	# Hand the loaded file to the player node so it has something to play.
	pickup_sound.stream = pickup_stream
	# The jump sound is built in code instead. There is no file on disk.
	jump_sound.stream = _make_beep(440.0, 0.1)
	# When something touches PickupArea, run "_on_pickup_collected".
	pickup_area.body_entered.connect(_on_pickup_collected)

# "body" is whatever touched the pickup area.
func _on_pickup_collected(body: Node2D) -> void:
	# "Player" is the class_name at the top of player.gd.
	if body is Player:
		pickup_sound.play()
		# Erase the pickup so it can only be collected once.
		pickup_area.queue_free()
		# "hud" was grabbed once at the top, by unique name, so this keeps
		# working even if the HUD is moved or tucked inside a group later.
		hud.show_message("Sound played!")

# "_input()" runs whenever the player presses a key. Godot calls it for you.
func _input(event: InputEvent) -> void:
	if event.is_action_pressed("jump"):
		jump_sound.play()

## Builds a short beep from scratch, with no sound file.
## "frequency" is the pitch: a bigger number makes a higher beep.
## "duration" is how many seconds the beep lasts.
func _make_beep(frequency: float, duration: float) -> AudioStreamWAV:
	# A sound is really just a long list of numbers describing how a speaker
	# should wiggle. "sample_rate" is how many numbers make one second.
	var sample_rate: int = 22050
	var sample_count: int = int(duration * sample_rate)
	var data: PackedByteArray = PackedByteArray()
	# Each number needs 2 bytes of room, so we ask for double the space.
	data.resize(sample_count * 2)
	for i in sample_count:
		# "t" is how far into the beep we are, measured in seconds.
		var t: float = float(i) / sample_rate
		# "sin()" draws a smooth up-and-down wave. That is exactly what a
		# pure musical tone looks like. The 0.5 makes it half as loud.
		var value: float = sin(t * frequency * TAU) * 0.5
		# Turn the wave (-1.0 to 1.0) into a whole number the speaker uses.
		var sample: int = int(value * 32767)
		# That number is too big to fit in one byte, so we split it into
		# two halves and store them side by side.
		data[i * 2] = sample & 0xFF
		data[i * 2 + 1] = (sample >> 8) & 0xFF
	# Wrap the finished list of numbers up as a sound Godot can play.
	var stream: AudioStreamWAV = AudioStreamWAV.new()
	stream.format = AudioStreamWAV.FORMAT_16_BITS
	stream.mix_rate = sample_rate
	stream.data = data
	return stream