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:
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.