Secuenciador de pasos

Una rejilla de notas clicable corriendo en bucle. Cada celda encendida suma una voz que decae al mismo buffer generado.

El editor de arriba está ejecutando este proyecto. Cambia una línea y se recarga.

Clases de Godot usadas

AudioStreamGeneratorButtonStyleBoxFlat

El código

scripts/main.gd
extends Node

const MIX_RATE: float = 22050.0
const STEPS: int = 16
const STEP_SECONDS: float = 0.16
const CELL: Vector2 = Vector2(46.0, 42.0)
const GAP: float = 8.0
# 0.9993 per sample fades a voice out in about a third of a second at 22050 Hz
const DECAY: float = 0.9993

const NOTES: Array = [
	{name = 'A4', hz = 440.00, steps = [3, 11]},
	{name = 'G4', hz = 392.00, steps = [5, 13]},
	{name = 'F4', hz = 349.23, steps = [2, 6, 10, 14]},
	{name = 'D4', hz = 293.66, steps = [4, 12]},
	{name = 'C4', hz = 261.63, steps = [0, 8]},
]

var player: AudioStreamPlayer
var playback: AudioStreamGeneratorPlayback
var voices: Array = []
var cells: Array = []
var playhead: ColorRect
var start_button: Button
var grid: Vector2
var step: int = 0
var elapsed: float = 0.0


func _ready() -> void:
	_add_background()

	var view: Vector2 = get_viewport().get_visible_rect().size
	var width: float = STEPS * (CELL.x + GAP) - GAP
	grid = Vector2((view.x - width) * 0.5, view.y * 0.3)

	_add_label('Step sequencer', Vector2(grid.x, view.y * 0.09), 26, Color('#ffffff'))
	_add_label('click any cell to change the pattern, every note is a decaying voice mixed into one generator', Vector2(grid.x, view.y * 0.15), 15, Color('#999999'))

	_add_grid()
	_add_player()

	start_button = Button.new()
	start_button.text = 'Start sound'
	start_button.position = Vector2(grid.x, grid.y + NOTES.size() * (CELL.y + GAP) + 28.0)
	start_button.pressed.connect(_toggle_sound)
	add_child(start_button)

	print('sequencer running, the playhead moves already, press Start sound to hear it')


func _process(delta: float) -> void:
	elapsed += delta

	if elapsed >= STEP_SECONDS:
		elapsed -= STEP_SECONDS
		step = (step + 1) % STEPS
		_play_column()

	playhead.position.x = grid.x + step * (CELL.x + GAP) - 3.0

	for row in cells:
		for cell in row:
			cell.modulate = cell.modulate.lerp(Color.WHITE, delta * 7.0)

	if playback != null:
		_fill_buffer()


func _play_column() -> void:
	for i in NOTES.size():
		var cell: Button = cells[i][step]

		if not cell.button_pressed:
			continue

		cell.modulate = Color(1.9, 1.9, 1.9)

		if playback != null:
			voices.append({phase = 0.0, hz = NOTES[i].hz, level = 0.6})


func _fill_buffer() -> void:
	for i in playback.get_frames_available():
		var sample: float = 0.0

		for voice in voices:
			voice.phase = fmod(voice.phase + voice.hz / MIX_RATE, 1.0)
			sample += (sin(voice.phase * TAU) + sin(voice.phase * TAU * 2.0) * 0.4) * voice.level
			voice.level *= DECAY

		playback.push_frame(Vector2(sample, sample) * 0.4)

	voices = voices.filter(func(voice: Dictionary) -> bool: return voice.level > 0.002)


func _toggle_sound() -> void:
	if playback != null:
		player.stop()
		playback = null
		voices.clear()
		start_button.text = 'Start sound'
		return

	player.play()
	playback = player.get_stream_playback()
	start_button.text = 'Stop sound'


func _add_grid() -> void:
	for i in NOTES.size():
		var top: float = grid.y + i * (CELL.y + GAP)
		var color: Color = _row_color(float(i) / float(NOTES.size() - 1))
		var row: Array = []

		_add_label(NOTES[i].name, Vector2(grid.x - 52.0, top + 8.0), 16, color)

		for at in STEPS:
			var cell: Button = _add_cell(Vector2(grid.x + at * (CELL.x + GAP), top), color)
			cell.button_pressed = NOTES[i].steps.has(at)
			row.append(cell)

		cells.append(row)

	playhead = ColorRect.new()
	playhead.color = Color('#f3d48d', 0.2)
	playhead.size = Vector2(CELL.x + 6.0, NOTES.size() * (CELL.y + GAP) - GAP + 6.0)
	playhead.position = Vector2(grid.x - 3.0, grid.y - 3.0)
	playhead.mouse_filter = Control.MOUSE_FILTER_IGNORE
	add_child(playhead)


func _add_cell(at: Vector2, color: Color) -> Button:
	var cell: Button = Button.new()
	cell.toggle_mode = true
	cell.position = at
	cell.custom_minimum_size = CELL
	cell.size = CELL
	cell.add_theme_stylebox_override('normal', _cell_style(Color('#2a2f3d')))
	cell.add_theme_stylebox_override('hover', _cell_style(Color('#3a4155')))
	cell.add_theme_stylebox_override('pressed', _cell_style(color))
	cell.add_theme_stylebox_override('hover_pressed', _cell_style(color.lightened(0.2)))
	cell.add_theme_stylebox_override('focus', StyleBoxEmpty.new())
	add_child(cell)

	return cell


func _cell_style(color: Color) -> StyleBoxFlat:
	var style: StyleBoxFlat = StyleBoxFlat.new()
	style.bg_color = color
	style.set_corner_radius_all(6)

	return style


func _row_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_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_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

Más ejemplos de Audio