I made an Envelope Generator Plugin which i just released the first ever alpha Version.
Please answer my Question after testing this Project.
The Data for its three Phases (Attack, Sustain, Release) are stored only in the EnvelopeGenerator Script.
Should the Settings for each Phase be moved out of the Script, into custom Resource Types?.
Integrating the Data for every Phase into the Script resembles the familiar UI of Music Maker Programs more closely
but moving it into Resources would make it much easier to swap and interpolate between Settings for each Phase in GDScript,
so i’m really not sure which is the better Decision.
Is there a way to read the code except downloading it from the Google drive link which you linked to in your other post? When I click the link, I see a zip file on a Google drive (see screenshot below). I would prefer to have some sort of Git server to look at that code, like Github or Codeberg.
I will make the Codeberg Repository after i fully applied the MIT License to the Project.
I just put it on Google Drive for now because i didn’t want Licensing to delay publishing the first few Dayly Versions.
GDScript for the EnvelopeGenerator Node Type (from Version Dayly 2026-08-19)
Should also work by itself, without installing the Plugin
GDScript
@tool
class_name EnvelopeGenerator
extends Node
signal sample_changed(sample: float)
signal started
signal released
signal finished
signal stopped
enum EnvelopeStages { STANDBY = 0, ATTACK, SUSTAIN, RELEASE }
@export var playing: bool = false:
set(value): playing = value
get: return playing
@export_tool_button('Play One Shot', 'Play') var _tool_play_one_shot: \
Callable = func() -> void: play_one_shot()
@export_tool_button(
'Stop', 'Stop'
) var _tool_stop: Callable = func() -> void: stop()
@export_group('Attack', 'attack_')
@export var attack_curve: Curve = preload(
'uid://raxr77od6vu0'
).duplicate():
set(value):
if value == null:
attack_curve = preload('uid://raxr77od6vu0').duplicate()
else:
attack_curve = value
get: return attack_curve
@export_tool_button(
'Set Curve to Instant', 'Curve'
) var attack__tool_set_curve_instant: Callable = func() -> void:
attack_curve = preload('uid://daddivl0bcppy').duplicate()
@export var attack_min_time: float = -1.0:
set(value):
attack_min_time = maxf(0.0, value)
if value < 0.0: attack_min_time = attack_curve.get_max_domain()
get: return attack_min_time
@export_tool_button(
'Set Min Time to Curve Length', 'Time'
) var attack__tool_min_time_to_curve_time: Callable = func() -> void:
attack_min_time = attack_curve.get_max_domain()
@export_group('Sustain', 'sustain_')
@export var sustain_min_time : float = 0.0:
set(value):
sustain_min_time = maxf(0.0, value)
get:
return sustain_min_time
@export_group('Release', 'release_')
@export var release_curve: Curve = preload(
'uid://7xipuiy21meh'
).duplicate():
set(value):
if value == null:
release_curve = preload('uid://7xipuiy21meh').duplicate()
else:
release_curve = value
@export_tool_button(
'Set Curve to Instant', 'Curve'
) var release__tool_set_curve_instant: Callable = func() -> void:
release_curve = preload('uid://duc85rm712bn8').duplicate()
var _envelope_stage: EnvelopeStages = EnvelopeStages.STANDBY
var _phase: float = 0.0
var sample: float
var _previous_sample: float
var _scaled_attack_curve
var _scaled_release_curve: Curve
var _scale_curve: Callable = func(curve: Curve) -> Curve:
var _scale_point_value: Callable = func(value: float) -> float:
return remap(
value,
curve.sample(curve.get_max_domain()),
curve.sample(curve.get_min_domain()),
curve.sample(curve.get_max_domain()),
sample,
)
var _output_curve: Curve = curve.duplicate()
for _point in curve.get_point_count():
var _y : float = curve.get_point_position(_point).y
_output_curve.set_point_value(
_point, _scale_point_value.call(_y)
)
return _output_curve
func set_playing(value: bool) -> void:
playing = value
func is_playing() -> bool:
return playing
func play() -> void:
stop()
playing = true
func play_one_shot(duration_sec: float = 0.0) -> void:
play()
await get_tree().create_timer( maxf(0.05, duration_sec) ).timeout
finish()
func finish() -> void:
set_playing(false)
func stop() -> void:
set_playing(false)
_envelope_stage = EnvelopeStages.STANDBY
func _ready() -> void:
_envelope_stage = EnvelopeStages.STANDBY
_phase = 0.0
func _process(delta: float) -> void:
var _advance_state: Callable = func() -> void:
match _envelope_stage:
EnvelopeStages.ATTACK:
_phase += delta
if _phase >= attack_curve.get_max_domain():
_phase = 0.0
_envelope_stage = EnvelopeStages.SUSTAIN
if not playing and _phase >= attack_min_time:
_phase = 0.0
_envelope_stage = EnvelopeStages.SUSTAIN
EnvelopeStages.SUSTAIN:
_phase += delta
if not playing and _phase >= sustain_min_time:
_scaled_release_curve = _scale_curve.call(release_curve)
released.emit()
_phase = 0.0
_envelope_stage = EnvelopeStages.RELEASE
EnvelopeStages.RELEASE:
_phase += delta
if _phase >= release_curve.get_max_domain():
playing = false
finished.emit()
_envelope_stage = EnvelopeStages.STANDBY
EnvelopeStages.STANDBY, _:
_phase = 0.0
if playing:
_scaled_attack_curve = _scale_curve.call(attack_curve)
started.emit()
_envelope_stage = EnvelopeStages.ATTACK
var _fetch_sample: Callable = func() -> float:
match _envelope_stage:
EnvelopeStages.ATTACK:
return _scaled_attack_curve.sample(_phase)
EnvelopeStages.SUSTAIN:
return _previous_sample
EnvelopeStages.RELEASE:
return _scaled_release_curve.sample(_phase)
EnvelopeStages.STANDBY, _:
return release_curve.sample(release_curve.get_max_domain())
_advance_state.call()
_previous_sample = sample
sample = _fetch_sample.call()
if not sample == _previous_sample:
sample_changed.emit(sample)
After short Research into electronic Synths,
about how Musicians morph between 2 Sets of Settings for every Phase of the Envelope.
It turns out, many pay hundreds of Euros for Synth Plugins like Serum and Omnisphere,
and use their Feature to interpolate between 2 Presets for every Phase of the Envelope.
So my original Assesment is only true for Users of free Synths:
Integrating the Data for every Phase into the Script resembles the familiar UI of Music Maker Programs more closely
For Users of many paid Synths, “moving it into Resources would” not only “make it much easier to swap and interpolate between Settings for each Phase in GDScript”
but also “resemble[] the familiar UI of Music Maker Programs more closely”.
Because Users of paid Synths use a Feature to morph between 2 Phase Presets while Users of free Synths can’t,
this raises a different Question:
Which Option would Users of a free Synth find easier to use?:
My Plugin keeping all Phase Data inside the Script