Spectrum bars
A generated saw sweep routed to its own bus and measured by AudioEffectSpectrumAnalyzer. The marker is the source frequency, the bars are what the analyzer reads.
The editor above is running this project. Change a line and it reloads.
Godot classes used
AudioEffectSpectrumAnalyzerAudioServerColorRect
The code
scripts/main.gd
extends Node
const MIX_RATE: float = 22050.0
const BANDS: int = 28
const LOW_HZ: float = 60.0
const HIGH_HZ: float = 9000.0
const SWEEP_LOW: float = 90.0
const SWEEP_HIGH: float = 1600.0
const SWEEP_SECONDS: float = 5.0
var player: AudioStreamPlayer
var playback: AudioStreamGeneratorPlayback
var analyzer: AudioEffectSpectrumAnalyzerInstance
var fills: Array[ColorRect] = []
var levels: PackedFloat32Array = PackedFloat32Array()
var marker: ColorRect
var readout: Label
var start_button: Button
var sweep: float = 0.0
var phase: float = 0.0
var wall: Rect2
func _ready() -> void:
_add_background()
var view: Vector2 = get_viewport().get_visible_rect().size
var size: Vector2 = Vector2(view.x * 0.78, view.y * 0.46)
wall = Rect2(Vector2((view.x - size.x) * 0.5, view.y * 0.24), size)
_add_label('Spectrum bars', Vector2(wall.position.x, view.y * 0.07), 26, Color('#ffffff'))
_add_label('a saw sweep on its own bus, measured by AudioEffectSpectrumAnalyzer', Vector2(wall.position.x, view.y * 0.13), 15, Color('#999999'))
levels.resize(BANDS)
_add_wall()
_add_axis()
_add_marker()
_add_player()
readout = _add_label('', Vector2(wall.position.x, wall.end.y + 40.0), 18, Color('#f3d48d'))
start_button = Button.new()
start_button.text = 'Start sound'
start_button.position = Vector2(wall.position.x, wall.end.y + 78.0)
start_button.pressed.connect(_toggle_sound)
add_child(start_button)
print('spectrum ready, the yellow marker is the source, press Start sound to fill the bars')
func _process(delta: float) -> void:
sweep = fmod(sweep + delta / SWEEP_SECONDS, 1.0)
var hz: float = _source_hz()
marker.position.x = wall.position.x + _band_at(hz) * wall.size.x - 13.0
readout.text = 'source %d Hz %s' % [int(hz), 'playing' if playback != null else 'silent, bars read zero']
_read_bands(delta)
if playback != null:
_fill_buffer(hz)
func _read_bands(delta: float) -> void:
for i in BANDS:
var low: float = _band_hz(i)
var high: float = _band_hz(i + 1)
var magnitude: float = analyzer.get_magnitude_for_frequency_range(low, high, AudioEffectSpectrumAnalyzerInstance.MAGNITUDE_MAX).length()
var target: float = clampf((70.0 + linear_to_db(magnitude)) / 70.0, 0.0, 1.0)
levels[i] = maxf(target, levels[i] - delta * 1.1)
var fill: ColorRect = fills[i]
var height: float = maxf(levels[i] * wall.size.y, 3.0)
fill.size.y = height
fill.position.y = wall.end.y - height
func _fill_buffer(hz: float) -> void:
for i in playback.get_frames_available():
phase = fmod(phase + hz / MIX_RATE, 1.0)
var sample: float = (phase * 2.0 - 1.0) * 0.16
playback.push_frame(Vector2(sample, sample))
func _source_hz() -> float:
var ping: float = 1.0 - absf(1.0 - sweep * 2.0)
return SWEEP_LOW * pow(SWEEP_HIGH / SWEEP_LOW, ping)
func _band_hz(index: int) -> float:
return LOW_HZ * pow(HIGH_HZ / LOW_HZ, float(index) / float(BANDS))
func _band_at(hz: float) -> float:
return clampf(log(hz / LOW_HZ) / log(HIGH_HZ / LOW_HZ), 0.0, 1.0)
func _toggle_sound() -> void:
if playback != null:
player.stop()
playback = null
start_button.text = 'Start sound'
return
player.play()
playback = player.get_stream_playback()
start_button.text = 'Stop sound'
func _add_wall() -> void:
var slot: float = wall.size.x / float(BANDS)
var width: float = slot - 6.0
for i in BANDS:
var left: float = wall.position.x + slot * i
var track: ColorRect = ColorRect.new()
track.color = Color('#2a2f3d')
track.position = Vector2(left, wall.position.y)
track.size = Vector2(width, wall.size.y)
add_child(track)
var fill: ColorRect = ColorRect.new()
fill.color = _band_color(float(i) / float(BANDS - 1))
fill.position = Vector2(left, wall.end.y - 3.0)
fill.size = Vector2(width, 3.0)
add_child(fill)
fills.append(fill)
func _band_color(at: float) -> Color:
if at < 0.5:
return Color('#8da5f3').lerp(Color('#8eef97'), at * 2.0)
return Color('#8eef97').lerp(Color('#f3d48d'), at * 2.0 - 1.0)
func _add_axis() -> void:
for hz in [60.0, 200.0, 800.0, 3000.0, 9000.0]:
var text: String = '%d Hz' % int(hz) if hz < 1000.0 else '%d kHz' % int(hz / 1000.0)
_add_label(text, Vector2(wall.position.x + _band_at(hz) * wall.size.x - 22.0, wall.end.y + 8.0), 13, Color('#999999'))
func _add_marker() -> void:
marker = ColorRect.new()
marker.color = Color('#f3d48d', 0.12)
marker.size = Vector2(26.0, wall.size.y + 26.0)
marker.position = Vector2(wall.position.x, wall.position.y - 13.0)
marker.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(marker)
var line: ColorRect = ColorRect.new()
line.color = Color('#f3d48d')
line.size = Vector2(2.0, marker.size.y)
line.position = Vector2(12.0, 0.0)
marker.add_child(line)
func _add_player() -> void:
var bus: int = _find_bus('Spectrum')
# a rerun in the same process finds the bus already built, and a second effect would leave a stale instance
if AudioServer.get_bus_effect_count(bus) == 0:
AudioServer.add_bus_effect(bus, AudioEffectSpectrumAnalyzer.new())
analyzer = AudioServer.get_bus_effect_instance(bus, 0)
var generator: AudioStreamGenerator = AudioStreamGenerator.new()
generator.mix_rate = MIX_RATE
generator.buffer_length = 0.1
player = AudioStreamPlayer.new()
player.stream = generator
player.bus = 'Spectrum'
add_child(player)
func _find_bus(bus: String) -> int:
var index: int = AudioServer.get_bus_index(bus)
if index != -1:
return index
index = AudioServer.get_bus_count()
AudioServer.add_bus(index)
AudioServer.set_bus_name(index, bus)
AudioServer.set_bus_send(index, 'Master')
return index
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