You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多
文档控制台
免费开始使用

ADSR包络Release阶段失效,按键松开后声音直接切断的问题求助

ADSR包络Release阶段失效,按键松开后声音直接切断的问题求助

看起来你的问题出在两个核心地方:包络的设计逻辑和按键松开后的波形生成处理,我帮你拆解一下并给出修复方案:


一、核心问题分析

1. 全局共享包络导致多音符Release异常

你现在用了一个全局的ADSREnvelope,但每个音符的按下、松开时间都是独立的——比如按下A键再按下B键,松开A键时A键应该进入自己的Release阶段衰减,而B键继续保持Sustain阶段。但全局包络只能对所有音符的总振幅进行控制,根本无法处理单个音符的独立包络逻辑,这是最关键的错误。

2. 按键松开时直接停止波形生成

当你松开某个按键,直接将keysPressed[i]设为false,导致该音符的波形立即停止生成,完全没有机会通过Release阶段衰减振幅,声音自然会直接切断。


二、具体修复方案

1. 给每个音符分配独立的ADSREnvelope

把原来的全局包络改成每个音符对应一个包络实例,这样每个音符的ADSR阶段都能独立控制。

2. 调整按键松开后的逻辑

按键松开时不直接停止波形生成,而是触发对应音符的Release阶段,直到该音符的振幅衰减到0后再停止生成波形。


三、修改后的关键代码片段

1. 全局变量部分修改

#include <iostream>
#include <cmath>
#include <vector>
#include <algorithm> // 用于max/min函数
#include "raylib/raylib.h"

using namespace std;

#define SIGNAL_SIZE 1024
#define SAMPLE_RATE 44100
#define BITS_PER_SAMPLE 16
#define NUM_KEYS 15
#define AMPLITUDE 8000

const float BASE_FREQUENCY = 220.0f;
const float twelfthRootOf2 = pow(2.0f, 1.0f / 12.0f);
float signal[SIGNAL_SIZE];
const float deltaTime = 1 / (float)SAMPLE_RATE;
double elapsedTime = 0.0;

struct ADSREnvelope {
    double attackTime;
    double decayTime;
    double releaseTime;
    double startAmplitude;
    double sustainAmplitude;
    double keydownTime;
    double keyupTime;
    bool notePressed;

    ADSREnvelope() {
        attackTime = 0.1; // 缩短Attack时间,更贴近真实乐器
        decayTime = 0.2;
        releaseTime = 1.0;
        startAmplitude = 1.0;
        sustainAmplitude = 0.7;
        keydownTime = 0.0;
        keyupTime = 0.0;
        notePressed = false;
    }

    void keydown(double _keydownTime) {
        keydownTime = _keydownTime;
        notePressed = true;
    }

    void keyup(double _keyupTime) {
        keyupTime = _keyupTime;
        notePressed = false;
    }

    double getAmplitude(double currTime) {
        double currAmp = 0.0;
        double lifetime = currTime - keydownTime;

        if (notePressed) {
            if (lifetime <= attackTime) {
                currAmp = (lifetime / attackTime) * startAmplitude;
            } else if (lifetime > attackTime && lifetime <= (attackTime + decayTime)) {
                currAmp = startAmplitude - ((lifetime - attackTime) / decayTime) * (startAmplitude - sustainAmplitude);
            } else {
                currAmp = sustainAmplitude;
            }
        } else {
            double releaseElapsed = currTime - keyupTime;
            if (releaseElapsed > releaseTime) {
                currAmp = 0.0;
            } else {
                currAmp = sustainAmplitude - (releaseElapsed / releaseTime) * sustainAmplitude;
            }
        }

        // 确保振幅在合法范围内
        currAmp = max(0.0, min(currAmp, startAmplitude));
        return currAmp;
    }
};

// 每个音符对应独立的包络、激活状态和相位
vector<ADSREnvelope> envelopes(NUM_KEYS);
vector<bool> keysDown(NUM_KEYS, false);
vector<bool> notesActive(NUM_KEYS, false);
vector<float> phases(NUM_KEYS, 0.0f);

2. 音频流更新函数修改

float sawWave(float phase) {
    return (2 * phase) - 1;
}

void updateStream(void* buffer, unsigned int frames) {
    short* d = (short*)buffer;
    for (unsigned int n = 0; n < frames; n++) {
        float sample = 0.0f;
        int activeNotes = 0;

        for (int i = 0; i < NUM_KEYS; i++) {
            if (notesActive[i]) {
                double amp = envelopes[i].getAmplitude(elapsedTime);
                // 振幅降到0,停止该音符的波形生成
                if (amp <= 0.00001) {
                    notesActive[i] = false;
                    continue;
                }

                float currFrequency = BASE_FREQUENCY * pow(twelfthRootOf2, i);
                float increment = currFrequency / SAMPLE_RATE;
                // 用当前音符的包络振幅混合波形
                sample += sawWave(phases[i]) * amp;
                phases[i] += increment;
                if (phases[i] > 1.0f) {
                    phases[i] -= 1.0f;
                }
                activeNotes++;
            }
        }

        // 混合多音符样本,避免音量过载
        if (activeNotes > 0) {
            sample /= activeNotes;
            sample *= 1.3; // 轻微补偿音量衰减
        } else {
            sample = 0.0f;
        }

        d[n] = static_cast<short>(sample * AMPLITUDE);
        elapsedTime += deltaTime;
    }
}

3. 主循环按键逻辑修改

int main(void) {
    const int screenWidth = 800;
    const int screenHeight = 450;
    InitWindow(screenWidth, screenHeight, "noyz");
    SetTargetFPS(60);

    // AUDIO初始化
    InitAudioDevice();
    SetAudioStreamBufferSizeDefault(SIGNAL_SIZE);
    AudioStream stream = LoadAudioStream(SAMPLE_RATE, BITS_PER_SAMPLE, 1);
    SetAudioStreamCallback(stream, updateStream);
    PlayAudioStream(stream);

    vector<string> notes = { "A", "A#", "B", "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B" };
    KeyboardKey keys[] = { KEY_Z, KEY_S, KEY_X, KEY_C, KEY_F, KEY_V, KEY_G, KEY_B, KEY_N, KEY_J, KEY_M, KEY_K, KEY_COMMA, KEY_L, KEY_PERIOD };

    while (!WindowShouldClose()) {
        bool play = false;
        string chord = "";

        for (int i = 0; i < NUM_KEYS; i++) {
            if (IsKeyDown(keys[i])) {
                play = true;
                chord += notes[i] + " - ";
                // 按键刚刚按下时触发包络的keydown
                if (!keysDown[i]) {
                    keysDown[i] = true;
                    envelopes[i].keydown(elapsedTime);
                    notesActive[i] = true;
                }
            } else {
                // 按键刚刚松开时触发包络的keyup
                if (keysDown[i]) {
                    keysDown[i] = false;
                    envelopes[i].keyup(elapsedTime);
                }
            }
        }

        // 绘制UI
        BeginDrawing();
        ClearBackground(BLACK);
        if (!chord.empty()) {
            chord.pop_back();
            chord.pop_back();
            chord.pop_back(); // 移除最后的" - "
        }
        Vector2 txtDim = MeasureTextEx(GetFontDefault(), chord.c_str(), 32, 0);
        DrawText(chord.c_str(), 400 - (txtDim.x / 2), 225 - (txtDim.y / 2), 32, RAYWHITE);
        EndDrawing();
    }

    CloseAudioDevice();
    CloseWindow();
    return 0;
}

四、修复后的效果

  1. 每个音符拥有独立的ADSR包络,按下时自然触发Attack->Decay->Sustain,松开时进入Release阶段,直到振幅降到0才停止发声,不会直接切断。
  2. 多音符同时按下、松开时,每个音符的衰减过程互不干扰,更贴近真实合成器的表现。
  3. 优化了ADSR参数的默认值,让声音过渡更自然。

你可以把这些修改应用到代码里,如果还有细节问题(比如波形类型调整、音量过载优化),随时再提出来~

火山引擎 最新活动