Audio Generation & ADSR Envelope

:information_source: Attention Topic was automatically imported from the old Question2Answer platform.
:bust_in_silhouette: Asked By creativeape

Hi, I’m building a fantasy console aka Pico-8, and I’m now trying to add a tracker and some synths.

I’ve managed to make some noises, and they sound alright but I really need to implement an ADSR envelope to smoothly transition between notes.

My attempts however have resulted in strange behavior, so I’m asking here if anyone could help.

The code is fairly simple, and adapted from whatever sources I could find online.
We have a Note class:

class_name Note extends Resource

enum NOTES {
	C3, Csharp3, D3, Dsharp3, E3, F3, Fsharp3, G3,
	Gsharp3, A3, Asharp3, B3, C4

const NOTE_FREQS = {
	"C3": 130.81,
	"Csharp3": 138.59,
	"D3": 146.83,
	"Dsharp3": 155.56,
	"E3": 164.81,
	"F3": 174.61,
	"Fsharp3": 185.00,
	"G3": 196.00,
	"Gsharp3": 207.65,
	"A3": 220.00,
	"Asharp3": 233.08,
	"B3": 246.94,
	"C4": 261.63,

export(NOTES) var note = 0

var pulse_hz:float = 440.0 setget set_pulse_hz, get_pulse_hz
var phase:float = 0.0
var increment:float

func _init():
	pulse_hz = NOTE_FREQS.values()[note]
	increment = pulse_hz / 22050

func get_note() -> float:
	return note

func set_pulse_hz(hz):
	pulse_hz = NOTE_FREQS.values()[note]
	increment = pulse_hz / 22050

func get_pulse_hz() -> float:
	return NOTE_FREQS.values()[note]

func frame() -> float:
	var result := sign(sin(phase * TAU))
	phase = fmod(phase + increment, 1.0)
	return result

#sin(2 * PI * freq * t + phase)

And then this class that plays those notes. (The SFXPattern class just holds an array of notes (or null for no note))

extends AudioStreamPlayer

export(Resource) var pattern

onready var _playback := get_stream_playback()
onready var _sample_hz:float = stream.mix_rate
onready var notes := []
onready var Clock = $Clock

var playhead:int = -1

var note_time:float = 0

var attack_time:float = 0.1
var release_time:float = 0.1

var note_volume:float = 0.0

func _ready():
	Clock.wait_time = 1.0 / 1
	Clock.connect("timeout", self, "next_note")
	for note in pattern.notes:
		if note != null:

func next_note():
	playhead += 1
	if playhead > pattern.length - 1:
		playhead = 0
	note_time = 0
	note_volume = 1
	print("playhead = " + str(playhead))

func _process(delta):
	note_time += delta
	var vol = note_volume
	if note_time > attack_time:
		# release
		vol = lerp(1.0, 0, (note_time - attack_time) / release_time)
		# attack
		vol = lerp(0.0, 1.0, note_time / attack_time)
	#note_volume = vol

func _fill_buffer():
	var note_count := notes.size()
	if note_count > 0:
		for frame_index in int(_playback.get_frames_available()):
			var frame := 0.0
			for note in notes:
				if note != null:
					frame += note.frame()

			_playback.push_frame((Vector2.ONE * frame / note_count) * note_volume)

There’s a timer that calls the next_note and pushes new notes to the buffer, as well as attempting to change volume over time, but it seems like the Timer is not synced properly to the audio frames? But I’m not sure how to fix that…

Thanks in advance!

I had problems with timing when experimenting with a drum-pad.
Investigating I realized that it was a common problem among those who wanted to make a music/rhythm game.
After several reports, a proposal (still open) was created on github:
Add an absolute time (DSP time) feature to play audio effects at specific intervals · Issue #1151 · godotengine/godot-proposals · GitHub

I don’t know if it applies to your problem. But you can read it to find temporary or alternative solutions. I also understand that in godot 4 an audio mixing mode and various improvements and features are implemented:

estebanmolca | 2022-08-20 13:26

Hmmm, I don’t think that’s the issue precicely? but that’s interesting nonetheless!

creativeape | 2022-08-20 15:11

:bust_in_silhouette: Reply From: creativeape

So, after some fiddling with this, I decided to go at i another route.

Instead of changing the volume property of the player and worrying about syncing it to the audio waveform stream, I decided to have volume baked into the generation of the audio frames.

It’s a bit complex, and the way I coded it is probably horrible (it works though!) but the relevant soure code is available on my github: GitHub - petterthowsen/epikus-8: Fantasy Console for making games made in Godot Engine (look in the /SFXer directory)