Tone generator
Sine, square and saw written sample by sample into an AudioStreamGenerator. The Line2D draws the same wave the buffer is being filled with.
The editor above is running this project. Change a line and it reloads.
Godot classes used
AudioStreamGeneratorAudioStreamPlayerLine2D
The code
scripts/main.gd
extends Node
const MIX_RATE: float = 22050.0
const POINTS: int = 260
const SHAPES: Array[String] = ['sine', 'square', 'saw']
var player: AudioStreamPlayer
var playback: AudioStreamGeneratorPlayback
var scope: Line2D
var readout: Label
var start_button: Button
var hz: float = 220.0
var shape: int = 0
var phase: float = 0.0
var scroll: float = 0.0
var box: Rect2
func _ready() -> void:
_add_background()
var view: Vector2 = get_viewport().get_visible_rect().size
var size: Vector2 = Vector2(view.x * 0.74, view.y * 0.44)
box = Rect2(Vector2((view.x - size.x) * 0.5, view.y * 0.24), size)
_add_label('Tone generator', Vector2(box.position.x, view.y * 0.08), 26, Color('#ffffff'))
_add_label('sine, square and saw written sample by sample into an AudioStreamGenerator', Vector2(box.position.x, view.y * 0.14), 15, Color('#999999'))
_add_scope()
_add_player()
readout = _add_label('', Vector2(box.position.x, box.end.y + 16.0), 18, Color('#f3d48d'))
_add_controls()
_refresh()
print('tone generator ready, press Start sound to hear the wave on screen')
func _process(delta: float) -> void:
scroll = fmod(scroll + delta * 0.4, 1.0)
_draw_scope()
if playback != null:
_fill_buffer()
func _fill_buffer() -> void:
for i in playback.get_frames_available():
phase = fmod(phase + hz / MIX_RATE, 1.0)
var sample: float = _wave(phase) * 0.22
playback.push_frame(Vector2(sample, sample))
func _wave(at: float) -> float:
match shape:
1:
return 1.0 if at < 0.5 else -1.0
2:
return at * 2.0 - 1.0
return sin(at * TAU)
func _draw_scope() -> void:
var cycles: float = hz / 110.0
var points: PackedVector2Array = PackedVector2Array()
for i in POINTS:
var at: float = float(i) / float(POINTS - 1)
var sample: float = _wave(fmod(scroll + at * cycles, 1.0))
points.append(Vector2(box.position.x + at * box.size.x, box.get_center().y - sample * box.size.y * 0.4))
scope.points = points
func _toggle_sound() -> void:
if playback != null:
player.stop()
playback = null
start_button.text = 'Start sound'
return
player.play()
# get_stream_playback() only answers after play()
playback = player.get_stream_playback()
start_button.text = 'Stop sound'
func _set_hz(value: float) -> void:
hz = value
_refresh()
func _set_shape(index: int) -> void:
shape = index
_refresh()
func _refresh() -> void:
readout.text = '%d Hz %s wave' % [int(hz), SHAPES[shape]]
func _add_player() -> void:
var generator: AudioStreamGenerator = AudioStreamGenerator.new()
generator.mix_rate = MIX_RATE
generator.buffer_length = 0.1
player = AudioStreamPlayer.new()
player.stream = generator
add_child(player)
func _add_scope() -> void:
var panel: ColorRect = ColorRect.new()
panel.color = Color('#2a2f3d')
panel.position = box.position
panel.size = box.size
add_child(panel)
var axis: ColorRect = ColorRect.new()
axis.color = Color('#8da5f3', 0.25)
axis.position = Vector2(box.position.x, box.get_center().y - 1.0)
axis.size = Vector2(box.size.x, 2.0)
add_child(axis)
scope = Line2D.new()
scope.width = 3.0
scope.default_color = Color('#8da5f3')
scope.joint_mode = Line2D.LINE_JOINT_ROUND
add_child(scope)
func _add_controls() -> void:
var row: HBoxContainer = HBoxContainer.new()
row.add_theme_constant_override('separation', 18)
row.position = Vector2(box.position.x, box.end.y + 58.0)
add_child(row)
var slider: HSlider = HSlider.new()
slider.min_value = 110.0
slider.max_value = 880.0
slider.value = hz
slider.custom_minimum_size = Vector2(280, 28)
slider.value_changed.connect(_set_hz)
row.add_child(slider)
var group: ButtonGroup = ButtonGroup.new()
for i in SHAPES.size():
var button: Button = Button.new()
button.text = SHAPES[i]
button.toggle_mode = true
button.button_group = group
button.button_pressed = i == shape
button.add_theme_color_override('font_pressed_color', Color('#8da5f3'))
button.add_theme_color_override('font_hover_pressed_color', Color('#8da5f3'))
button.pressed.connect(_set_shape.bind(i))
row.add_child(button)
# the browser keeps audio silent until a real click, so the tone waits for this button
start_button = Button.new()
start_button.text = 'Start sound'
start_button.pressed.connect(_toggle_sound)
row.add_child(start_button)
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