0 votes

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!

Godot version 3.5
in Engine by (120 points)

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:
https://github.com/godotengine/godot-proposals/issues/1151

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:
https://github.com/godotengine/godot/pull/64488

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

1 Answer

0 votes

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: https://github.com/petterthowsen/epikus-8 (look in the /SFXer directory)

by (120 points)
Welcome to Godot Engine Q&A, where you can ask questions and receive answers from other members of the community.

Please make sure to read Frequently asked questions and How to use this Q&A? before posting your first questions.
Social login is currently unavailable. If you've previously logged in with a Facebook or GitHub account, use the I forgot my password link in the login box to set a password for your account. If you still can't access your account, send an email to [email protected] with your username.