Generate a coin pickup sound effect entirely in code by writing raw PCM samples into an AudioStreamWAV. No .wav, .ogg, or .mp3 assets required.

The idea

Instead of hunting for sound effect assets, synthesise them at runtime. A coin “ding” is just two sine waves (a note + a perfect fifth) with a quick attack and exponential decay. Write the samples into a buffer, hand it to an AudioStreamPlayer, and you’re done.

This approach also makes it trivial to vary the sound: pitch, duration, timbre, without maintaining multiple files.

Setup

Create two nodes in your scene tree:

1
2
Node (script: Sfx.gd)          <- add as an Autoload
└── (AudioStreamPlayers created in code)

Register Sfx as an Autoload in Project -> Project Settings -> Autoload.

The code

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# sfx.gd - register as Autoload named "Sfx"
extends Node

const VOICES := 4            # round-robin players so rapid pickups overlap cleanly
const RESET_MS := 1200       # gap (ms) with no pickup that resets the combo pitch
const PITCH_STEP := 0.07     # pitch increase per consecutive pickup
const MAX_COMBO := 14        # cap so it doesn't get shrill
const HAPTIC_MS := 15

var _players: Array[AudioStreamPlayer] = []
var _voice := 0
var _combo := 0
var _last_msec := -100000


func _ready() -> void:
	var snd := _make_coin_sound()
	for i in VOICES:
		var p := AudioStreamPlayer.new()
		p.stream = snd
		p.volume_db = -5.0
		add_child(p)
		_players.append(p)


## Call this from anywhere to play the coin sound with combo-pitch.
func play_coin() -> void:
	var now := Time.get_ticks_msec()
	_combo = 0 if now - _last_msec > RESET_MS else mini(_combo + 1, MAX_COMBO)
	_last_msec = now

	var p := _players[_voice]
	_voice = (_voice + 1) % VOICES
	p.pitch_scale = 1.0 + _combo * PITCH_STEP
	p.play()

	Input.vibrate_handheld(HAPTIC_MS)


## Builds a short bright "ding" from two layered sine waves.
func _make_coin_sound() -> AudioStreamWAV:
	var rate := 22050
	var dur := 0.14
	var n := int(rate * dur)
	var f1 := 1245.0          # base note
	var f2 := f1 * 1.5        # a fifth above, for richness
	var data := PackedByteArray()
	data.resize(n * 2)        # 16-bit mono = 2 bytes per sample
	for i in n:
		var t := float(i) / rate
		var attack := minf(t / 0.004, 1.0)          # 4ms fade-in avoids a click
		var env := attack * exp(-t * 22.0)          # quick decay
		var s := (sin(TAU * f1 * t) * 0.7 + sin(TAU * f2 * t) * 0.3) * env * 0.6
		data.encode_s16(i * 2, int(clampf(s, -1.0, 1.0) * 32767.0))

	var wav := AudioStreamWAV.new()
	wav.format = AudioStreamWAV.FORMAT_16_BITS
	wav.mix_rate = rate
	wav.stereo = false
	wav.data = data
	return wav

Usage

From any script, call:

1
Sfx.play_coin()

Collecting coins in rapid succession raises the pitch each time. A 1.2-second gap resets the combo back to the base note.

Tuning tips

  • Lower pitch? Drop f1 to around 800 Hz for a deeper thunk.
  • Longer ring? Increase dur and lower the decay constant (22 -> 12).
  • Metallic sound? Add a third sine at f1 * 2.0 (octave) at 15% volume.
  • Different sound per item? Make _make_coin_sound() accept frequency/duration params and cache different streams.

Requirements

  • Godot 4.x (uses AudioStreamWAV.encode_s16, available since 4.0).
  • No plugins, no assets, no external dependencies.