Barres de spectre
Un balayage généré routé vers son propre bus et mesuré par AudioEffectSpectrumAnalyzer. Le repère est la fréquence source, les barres sont ce que lit l'analyseur.
L'éditeur ci-dessus fait tourner ce projet. Changez une ligne et il se recharge.
Classes Godot utilisées
AudioEffectSpectrumAnalyzerAudioServerColorRect
Le 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