位置音频
一个带 max_distance 和 attenuation 的移动音源,旁边的条形图显示 AudioStreamPlayer2D 对生成音施加的同一条衰减曲线。
上方的编辑器正在运行这个项目。改一行代码,它就会重新加载。
用到的 Godot 类
AudioStreamPlayer2DAudioListener2DLine2D
代码
scripts/main.gd
extends Node
const MIX_RATE: float = 22050.0
const TONE_HZ: float = 330.0
const ATTENUATION: float = 1.6
const BAR: float = 260.0
var source: Node2D
var speaker: AudioStreamPlayer2D
var link: Line2D
var volume_bar: ColorRect
var pan_marker: ColorRect
var readout: Label
var start_button: Button
var center: Vector2
var range_radius: float
var elapsed: float = 0.0
func _ready() -> void:
_add_background()
var view: Vector2 = get_viewport().get_visible_rect().size
center = Vector2(view.x * 0.5, view.y * 0.52)
range_radius = minf(view.x, view.y) * 0.33
_add_label('Positional audio', Vector2(48.0, view.y * 0.07), 26, Color('#ffffff'))
_add_label('the circle is max_distance, and the bar is the same falloff AudioStreamPlayer2D applies', Vector2(48.0, view.y * 0.13), 15, Color('#999999'))
_add_field()
_add_listener()
_add_source()
_add_meters(Vector2(48.0, view.y * 0.78))
start_button = Button.new()
start_button.text = 'Start sound'
start_button.position = Vector2(48.0, view.y * 0.9)
start_button.pressed.connect(_toggle_sound)
add_child(start_button)
print('the source is already orbiting, press Start sound to hear it fade with distance')
func _process(delta: float) -> void:
elapsed += delta
source.position = center + Vector2(cos(elapsed * 0.7 + 0.5) * range_radius * 1.15, sin(elapsed * 1.1 + 1.9) * range_radius * 0.8)
source.scale = Vector2.ONE * (1.0 + sin(elapsed * 7.0) * 0.12)
var distance: float = source.position.distance_to(center)
var volume: float = pow(1.0 - clampf(distance / range_radius, 0.0, 1.0), ATTENUATION)
var pan: float = clampf((source.position.x - center.x) / range_radius, -1.0, 1.0)
link.points = PackedVector2Array([center, source.position])
link.default_color = Color('#2a2f3d').lerp(Color('#8eef97'), volume)
volume_bar.size.x = maxf(volume * BAR, 2.0)
pan_marker.position.x = 48.0 + (pan * 0.5 + 0.5) * BAR - 3.0
readout.text = 'distance %d px volume %.2f pan %+.2f' % [int(distance), volume, pan]
func _toggle_sound() -> void:
if speaker.playing:
speaker.stop()
start_button.text = 'Start sound'
return
speaker.play()
start_button.text = 'Stop sound'
func _add_field() -> void:
for at in [1.0, 0.5]:
var circle: Line2D = _make_circle(range_radius * at, Color('#8da5f3', 0.45 - at * 0.2))
circle.position = center
add_child(circle)
link = Line2D.new()
link.width = 3.0
add_child(link)
func _add_listener() -> void:
var ear: Node2D = Node2D.new()
ear.position = center
ear.add_child(_make_disc(16.0, Color('#f3d48d')))
add_child(ear)
var listener: AudioListener2D = AudioListener2D.new()
ear.add_child(listener)
listener.make_current()
_add_label('AudioListener2D', center + Vector2(-58.0, 24.0), 14, Color('#999999'))
func _add_source() -> void:
source = Node2D.new()
source.add_child(_make_disc(18.0, Color('#8da5f3')))
add_child(source)
speaker = AudioStreamPlayer2D.new()
speaker.stream = _make_tone()
speaker.max_distance = range_radius
speaker.attenuation = ATTENUATION
speaker.panning_strength = 1.4
speaker.volume_db = -8.0
source.add_child(speaker)
func _add_meters(at: Vector2) -> void:
readout = _add_label('', at - Vector2(0.0, 30.0), 18, Color('#f3d48d'))
var track: ColorRect = ColorRect.new()
track.color = Color('#2a2f3d')
track.position = at
track.size = Vector2(BAR, 16.0)
add_child(track)
volume_bar = ColorRect.new()
volume_bar.color = Color('#8eef97')
volume_bar.position = at
volume_bar.size = Vector2(2.0, 16.0)
add_child(volume_bar)
var pan_track: ColorRect = ColorRect.new()
pan_track.color = Color('#2a2f3d')
pan_track.position = at + Vector2(0.0, 28.0)
pan_track.size = Vector2(BAR, 10.0)
add_child(pan_track)
pan_marker = ColorRect.new()
pan_marker.color = Color('#8da5f3')
pan_marker.position = at + Vector2(BAR * 0.5 - 3.0, 24.0)
pan_marker.size = Vector2(6.0, 18.0)
add_child(pan_marker)
func _make_tone() -> AudioStreamWAV:
var period: int = int(round(MIX_RATE / TONE_HZ))
var frames: int = period * 40
var data: PackedByteArray = PackedByteArray()
data.resize(frames * 2)
for i in frames:
var phase: float = float(i % period) / float(period)
var sample: float = sin(phase * TAU) * 0.7 + (phase * 2.0 - 1.0) * 0.2
data.encode_s16(i * 2, int(sample * 24000.0))
var wav: AudioStreamWAV = AudioStreamWAV.new()
wav.format = AudioStreamWAV.FORMAT_16_BITS
wav.mix_rate = int(MIX_RATE)
wav.stereo = false
wav.data = data
wav.loop_mode = AudioStreamWAV.LOOP_FORWARD
wav.loop_begin = 0
wav.loop_end = frames
return wav
func _make_disc(radius: float, color: Color) -> Polygon2D:
var points: PackedVector2Array = PackedVector2Array()
for i in 30:
points.append(Vector2(cos(TAU * i / 30.0), sin(TAU * i / 30.0)) * radius)
var disc: Polygon2D = Polygon2D.new()
disc.polygon = points
disc.color = color
return disc
func _make_circle(radius: float, color: Color) -> Line2D:
var circle: Line2D = Line2D.new()
circle.closed = true
circle.width = 2.0
circle.default_color = color
for i in 48:
circle.add_point(Vector2(cos(TAU * i / 48.0), sin(TAU * i / 48.0)) * radius)
return circle
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