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