步进音序器
一个可点击的音符网格循环播放。每个点亮的格子都会往同一个生成缓冲区里加入一个衰减的音。
上方的编辑器正在运行这个项目。改一行代码,它就会重新加载。
用到的 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