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
property_list_changed_notify()
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")
Clock.start()
_fill_buffer()
for note in pattern.notes:
if note != null:
print(note.pulse_hz)
func next_note():
playhead += 1
if playhead > pattern.length - 1:
playhead = 0
notes.clear()
notes.append(pattern.notes[playhead])
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)
else:
# attack
vol = lerp(0.0, 1.0, note_time / attack_time)
#note_volume = vol
_fill_buffer()
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!