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

More Audio examples