Collision sounds
Bodies falling through pegs, with one blip built in code and retuned by pitch_scale from the speed each body carried into the impact.
The editor above is running this project. Change a line and it reloads.
Godot classes used
AudioStreamWAVRigidBody2DAudioStreamPlayer
The code
scripts/main.gd
extends Node
const MIX_RATE: float = 22050.0
const BASE_HZ: float = 196.0
const BALLS: int = 8
const RADIUS: float = 16.0
const PEG_RADIUS: float = 26.0
const VOICES: int = 8
const FAST: float = 900.0
const QUIET: float = 70.0
var players: Array[AudioStreamPlayer] = []
var balls: Array[RigidBody2D] = []
# RigidBody2D -> speed measured on the frame before the physics step
var speeds: Dictionary = {}
var next_player: int = 0
var sound_on: bool = false
var last_hit: float = 0.0
var meter: ColorRect
var readout: Label
var start_button: Button
var view: Vector2
func _ready() -> void:
_add_background()
view = get_viewport().get_visible_rect().size
_add_label('Collision sounds', Vector2(48.0, view.y * 0.07), 26, Color('#ffffff'))
_add_label('one generated blip, retuned by pitch_scale from the speed the body carried into the hit', Vector2(48.0, view.y * 0.13), 15, Color('#999999'))
_add_pegs()
_add_ramps()
_add_players()
for i in BALLS:
_add_ball(i)
_add_meter()
start_button = Button.new()
start_button.text = 'Enable sound'
start_button.position = Vector2(48.0, view.y * 0.56)
start_button.pressed.connect(_toggle_sound)
add_child(start_button)
print('bodies are already falling, press Enable sound to hear every impact')
func _physics_process(_delta: float) -> void:
for ball in balls:
speeds[ball] = ball.linear_velocity.length()
if ball.position.y > view.y + 120.0 or ball.position.x < -120.0 or ball.position.x > view.x + 120.0:
_drop(ball)
func _process(delta: float) -> void:
last_hit = maxf(0.0, last_hit - delta * 0.9)
meter.size.x = maxf(last_hit * 260.0, 2.0)
func _on_hit(_other: Node, ball: RigidBody2D) -> void:
var speed: float = speeds.get(ball, 0.0)
if speed < QUIET:
return
var strength: float = clampf(speed / FAST, 0.0, 1.0)
last_hit = strength
readout.text = 'impact %d px/s pitch %.2fx' % [int(speed), _pitch(strength)]
_ring(ball.position, strength)
if not sound_on:
return
var player: AudioStreamPlayer = players[next_player]
next_player = (next_player + 1) % VOICES
player.pitch_scale = _pitch(strength)
player.volume_db = lerpf(-22.0, -5.0, strength)
player.play()
func _pitch(strength: float) -> float:
return lerpf(0.65, 2.3, strength)
func _ring(at: Vector2, strength: float) -> void:
var ring: Line2D = _make_circle(RADIUS + 6.0, Color('#f3d48d'))
ring.position = at
add_child(ring)
var tween: Tween = create_tween().set_parallel()
tween.tween_property(ring, 'scale', Vector2.ONE * (1.1 + strength * 1.2), 0.35)
tween.tween_property(ring, 'modulate:a', 0.0, 0.35)
tween.chain().tween_callback(ring.queue_free)
func _toggle_sound() -> void:
sound_on = not sound_on
start_button.text = 'Mute' if sound_on else 'Enable sound'
func _add_ball(index: int) -> void:
var material: PhysicsMaterial = PhysicsMaterial.new()
material.bounce = 0.62
material.friction = 0.2
var ball: RigidBody2D = RigidBody2D.new()
ball.physics_material_override = material
ball.contact_monitor = true
ball.max_contacts_reported = 4
var shape: CollisionShape2D = CollisionShape2D.new()
var circle: CircleShape2D = CircleShape2D.new()
circle.radius = RADIUS
shape.shape = circle
ball.add_child(shape)
ball.add_child(_make_disc(RADIUS, Color('#8da5f3')))
add_child(ball)
ball.body_entered.connect(_on_hit.bind(ball))
balls.append(ball)
ball.position = Vector2(randf_range(view.x * 0.3, view.x * 0.7), -60.0 - index * 130.0)
func _drop(ball: RigidBody2D) -> void:
ball.linear_velocity = Vector2.ZERO
ball.angular_velocity = 0.0
ball.position = Vector2(randf_range(view.x * 0.3, view.x * 0.7), -60.0 - randf() * 160.0)
func _add_pegs() -> void:
for row in 3:
var count: int = 5 - row % 2
var top: float = view.y * (0.34 + row * 0.16)
for i in count:
var at: Vector2 = Vector2(view.x * (0.5 + (i - (count - 1) * 0.5) * 0.13), top)
var peg: StaticBody2D = StaticBody2D.new()
peg.position = at
var shape: CollisionShape2D = CollisionShape2D.new()
var circle: CircleShape2D = CircleShape2D.new()
circle.radius = PEG_RADIUS
shape.shape = circle
peg.add_child(shape)
peg.add_child(_make_disc(PEG_RADIUS, Color('#2a2f3d')))
add_child(peg)
func _add_ramps() -> void:
for side in [-1.0, 1.0]:
var ramp: StaticBody2D = StaticBody2D.new()
ramp.position = Vector2(view.x * (0.5 + side * 0.3), view.y * 0.8)
ramp.rotation = -side * 0.34
var box: RectangleShape2D = RectangleShape2D.new()
box.size = Vector2(view.x * 0.34, 20.0)
var shape: CollisionShape2D = CollisionShape2D.new()
shape.shape = box
ramp.add_child(shape)
var plank: Polygon2D = Polygon2D.new()
plank.polygon = PackedVector2Array([-box.size * 0.5, Vector2(box.size.x, -box.size.y) * 0.5, box.size * 0.5, Vector2(-box.size.x, box.size.y) * 0.5])
plank.color = Color('#2a2f3d')
ramp.add_child(plank)
add_child(ramp)
func _add_meter() -> void:
readout = _add_label('waiting for the first impact', Vector2(48.0, view.y * 0.4), 18, Color('#f3d48d'))
var track: ColorRect = ColorRect.new()
track.color = Color('#2a2f3d')
track.position = Vector2(48.0, view.y * 0.46)
track.size = Vector2(260.0, 14.0)
add_child(track)
meter = ColorRect.new()
meter.color = Color('#8eef97')
meter.position = track.position
meter.size = Vector2(2.0, 14.0)
add_child(meter)
func _add_players() -> void:
var blip: AudioStreamWAV = _make_blip()
for i in VOICES:
var player: AudioStreamPlayer = AudioStreamPlayer.new()
player.stream = blip
add_child(player)
players.append(player)
func _make_blip() -> AudioStreamWAV:
var frames: int = int(MIX_RATE * 0.22)
var data: PackedByteArray = PackedByteArray()
data.resize(frames * 2)
for i in frames:
var at: float = float(i) / float(frames)
var seconds: float = float(i) / MIX_RATE
var envelope: float = pow(1.0 - at, 3.0)
var sample: float = (sin(TAU * BASE_HZ * seconds) * 0.7 + sin(TAU * BASE_HZ * 2.0 * seconds) * 0.3) * envelope
data.encode_s16(i * 2, int(sample * 26000.0))
var wav: AudioStreamWAV = AudioStreamWAV.new()
wav.format = AudioStreamWAV.FORMAT_16_BITS
wav.mix_rate = int(MIX_RATE)
wav.stereo = false
wav.data = data
return wav
func _make_disc(radius: float, color: Color) -> Polygon2D:
var points: PackedVector2Array = PackedVector2Array()
for i in 28:
points.append(Vector2(cos(TAU * i / 28.0), sin(TAU * i / 28.0)) * radius)
var disc: Polygon2D = Polygon2D.new()
disc.polygon = points
disc.color = color
return disc
func _make_circle(radius: float, color: Color) -> Line2D:
var ring: Line2D = Line2D.new()
ring.closed = true
ring.width = 3.0
ring.default_color = color
for i in 28:
ring.add_point(Vector2(cos(TAU * i / 28.0), sin(TAU * i / 28.0)) * radius)
return ring
func _add_background() -> void:
var background: ColorRect = ColorRect.new()
background.color = Color('#12141a')
add_child(background)
background.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
func _add_label(text: String, at: Vector2, size: int, color: Color) -> Label:
var label: Label = Label.new()
label.text = text
label.position = at
label.add_theme_font_size_override('font_size', size)
label.add_theme_color_override('font_color', color)
add_child(label)
return label