Sons de collision

Des corps tombant entre des picots, avec un blip créé en code et réaccordé par pitch_scale selon la vitesse que chaque corps avait à l'impact.

L'éditeur ci-dessus fait tourner ce projet. Changez une ligne et il se recharge.

Classes Godot utilisées

AudioStreamWAVRigidBody2DAudioStreamPlayer

Le 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

Plus d'exemples de Audio