You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

解决Android平台JSyn单音采样器包络重触发爆音问题

无延迟解决JSyn单音采样器爆音问题的方案

我之前在JSyn开发单音采样器时也碰到过完全一样的爆音问题——直接调用synth.stop()会立刻切断音频流,跳过包络的release衰减阶段,导致音量突变产生刺耳的爆音。你用的增益置0+固定休眠的方法虽然能消爆音,但硬编码的延迟确实会影响交互体验,这里给你几个更优雅的无延迟解决方案:

方案1:触发旧音符的Release包络,自然衰减后再清理实例

核心思路是不让旧音符突然停止,而是主动触发它的release包络,等音量自然降到0后再清理旧实例,完全不需要休眠。JSyn的SegmentedEnvelope可以通过队列指令触发release阶段,再监听包络输出判断何时完成衰减:

修改监听代码

buttonNoteC1Get.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_UP) {
            buttonNoteC1Get.setBackgroundColor(myColorWhite);
            if (sample.getSustainBegin() > 0) {
                ampEnv.dataQueue.queue(ampEnvelope, 3, 1);
            }
            limit = 0;
            return true;
        }
        if (limit == 0) {
            buttonNoteC1Get.setBackgroundColor(myColorGrey);
            samplePitch = octave * 1;

            if (samplePlayer != null) {
                // 触发旧音符的release包络
                ampEnv.dataQueue.clear();
                ampEnv.dataQueue.queue(ampEnvelope, 3, 1); // 指向你的release段

                // 监听包络输出,降到接近0时清理旧实例并启动新音符
                ampEnv.output.addPortListener(new PortListener() {
                    @Override
                    public void update(Port port) {
                        double currentLevel = port.getValue();
                        // 当音量低于阈值(比如0.001,避免浮点误差)
                        if (currentLevel < 0.001) {
                            // 清理旧组件
                            synth.remove(samplePlayer);
                            synth.remove(ampEnv);
                            // 移除监听防止内存泄漏
                            port.removePortListener(this);
                            samplePlayer = null;
                            // 启动新音符
                            Sampler.player();
                            Sampler.play();
                        }
                    }
                });
            } else {
                // 首次播放直接初始化
                Sampler.player();
                Sampler.play();
            }

            limit = 1;
        }
        return false;
    }
});

这个方案完全模拟了自然的音符释放过程,用户不会感觉到任何延迟,同时彻底避免爆音。

方案2:复用采样器实例,避免频繁销毁重建

每次触发新音符就销毁重建samplePlayer等组件不仅容易出问题,还会浪费资源。更好的方式是只初始化一次组件,触发新音符时直接重置参数并重新播放:

修改Sampler类

public class Sampler {
    public static VariableRateDataReader samplePlayer;
    public static LineOut lineOut;
    public static FloatSample sample;
    public static SegmentedEnvelope ampEnvelope;
    public static VariableRateMonoReader ampEnv;
    public static MixerMonoRamped mixerMono;
    public static double[] ampData;
    public static FilterStateVariable mMainFilter;
    public static Synthesizer synth = JSyn.createSynthesizer(new JSynAndroidAudioDevice());
    // 标记是否已初始化组件
    private static boolean isVoiceInitialized = false;

    static void loadSample(){
        SampleLoader.setJavaSoundPreferred(false);
        try {
            sample = SampleLoader.loadFloatSample(sampleFile);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // 只执行一次的初始化方法
    static void initVoice() {
        if (isVoiceInitialized) return;

        // 创建包络数据
        ampData = new double[] {
            envA, 0.9,  // Attack
            envD, envS,  // Decay
            0, envS,     // Sustain
            envR, 0.0,   // Release
        };
        ampEnvelope = new SegmentedEnvelope(ampData);

        // 添加组件到 synth
        synth.add(ampEnv = new VariableRateMonoReader());
        synth.add(lineOut = new LineOut());
        synth.add(mixerMono = new MixerMonoRamped(2));
        synth.add(mMainFilter = new FilterStateVariable());

        // 连接信号链路
        mixerMono.output.connect(mMainFilter.input);
        mMainFilter.output.connect(0, lineOut.input, 0);
        mMainFilter.output.connect(0, lineOut.input, 1);

        // 根据采样声道创建播放器
        if (sample.getChannelsPerFrame() == 1) {
            synth.add(samplePlayer = new VariableRateMonoReader());
            samplePlayer.output.connect(0, mixerMono.input, 0);
            samplePlayer.output.connect(0, mixerMono.input, 1);
        } else if (sample.getChannelsPerFrame() == 2) {
            synth.add(samplePlayer = new VariableRateStereoReader());
            samplePlayer.output.connect(0, mixerMono.input, 0);
            samplePlayer.output.connect(1, mixerMono.input, 1);
        } else {
            throw new RuntimeException("仅支持单声道/立体声采样");
        }

        // 连接包络到播放器增益
        ampEnv.output.connect(samplePlayer.amplitude);

        // 启动 synth 和输出
        synth.start();
        lineOut.start();
        isVoiceInitialized = true;
    }

    // 播放新音符,复用现有实例
    public static void play(double newPitch) {
        if (!isVoiceInitialized) initVoice();

        // 清空旧的播放队列,避免残留
        samplePlayer.dataQueue.clear();
        ampEnv.dataQueue.clear();

        // 设置新音高
        samplePlayer.rate.set(sample.getFrameRate() * newPitch);

        // 更新实时参数(比如滤镜、音量)
        mixerMono.amplitude.set(sliderVal / 100.0f);
        mMainFilter.frequency.set(mainFilterCutFloat);
        mMainFilter.resonance.set(mainFilterResFloat);

        // 播放新音符的包络和采样
        if (sample.getSustainBegin() < 0) {
            samplePlayer.dataQueue.queue(sample);
            ampEnv.dataQueue.queue(ampEnvelope);
        } else {
            samplePlayer.dataQueue.queueOn(sample);
            ampEnv.dataQueue.queue(ampEnvelope, 0,3);
            ampEnv.dataQueue.queueLoop(ampEnvelope, 1, 2);
        }

        ampEnv.start();
    }
}

修改监听代码

if (limit == 0) {
    buttonNoteC1Get.setBackgroundColor(myColorGrey);
    samplePitch = octave * 1;
    // 直接复用实例播放新音符,无需销毁旧组件
    Sampler.play(samplePitch);
    limit = 1;
}

这个方案不仅解决了爆音问题,还大幅提升了性能——不需要频繁创建/销毁Synth组件,交互响应完全无延迟,是单音采样器的最优实践。

方案3:平滑衰减增益而非突变

如果你暂时不想修改整体架构,可以把固定休眠替换为基于release时间的平滑衰减:

if (samplePlayer != null) {
    // 用release时间作为增益衰减的时长,平滑降到0
    mixerMono.amplitude.set(0, envR);
    // 不需要固定休眠,而是监听增益降到0后再stop
    mixerMono.amplitude.addPortListener(new PortListener() {
        @Override
        public void update(Port port) {
            if (port.getValue() < 0.001) {
                synth.stop();
                port.removePortListener(this);
                // 启动新音符
                Sampler.player();
                Sampler.play();
            }
        }
    });
}

这样可以避免硬编码的70ms延迟,而是根据你的release参数动态调整衰减时间,体验会好很多。

内容的提问来源于stack exchange,提问作者Yaron Faxi Nadu Eshkar

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.28 10:15:42