步进音序器

一个可点击的音符网格循环播放。每个点亮的格子都会往同一个生成缓冲区里加入一个衰减的音。

上方的编辑器正在运行这个项目。改一行代码,它就会重新加载。

用到的 Godot 类

AudioStreamGeneratorButtonStyleBoxFlat

代码

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

更多音频示例