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

如何在AudioKit中构建带频率/音高包络的振荡器?

嘿,作为AudioKit新手,你这个思路抓得特别准——把ADSR包络映射到频率这类参数上,确实没法直接用现成的振荡器Bank(它们默认只把ADSR连到音量增益上),得自己动手改改DSP层面的路由逻辑。其实你说的“借鉴AKxxxOscillatorBanks的代码,把ADSR应用到音高”完全可行,下面给你具体的实现思路和代码示例:

核心思路拆解

现成的振荡器Bank本质是给每个音符分配一个振荡器+一个ADSR包络,然后把包络输出连到振荡器的增益参数。我们要做的就是:

  • 保留“多复音+每个音符对应独立包络”的结构
  • 把包络的输出目标从「增益」换成「频率」(或者你需要的其他参数,比如滤波器截止频率、相位等)
具体实现步骤与代码示例

下面是一个简化版的「支持频率ADSR的正弦振荡器Bank」,你可以基于这个扩展成其他波形的版本:

import AudioKit

class ADSRFrequencyOscillatorBank: AKPolyphonicNode {
    // 存储每个复音对应的振荡器和包络生成器
    private var oscillators: [AKSineOscillator] = []
    private var envelopeGenerators: [AKEnvelopeGenerator] = []
    // 可配置的最大频率偏移范围(单位:Hz)
    var maxFrequencyOffset: Double = 500.0

    // 初始化:指定最大复音数,创建对应数量的振荡器和包络
    init(maxPolyphony: Int = 8, maxFrequencyOffset: Double = 500.0) {
        self.maxFrequencyOffset = maxFrequencyOffset
        super.init()

        for _ in 0..<maxPolyphony {
            let osc = AKSineOscillator()
            let env = AKEnvelopeGenerator()
            
            // 关键:把包络输出映射到频率偏移,再连到振荡器的频率参数
            // 包络输出范围是0-1,乘以偏移范围后得到0到maxFrequencyOffset的偏移量
            env.output.map { $0 * maxFrequencyOffset } >>> osc.frequency
            
            oscillators.append(osc)
            envelopeGenerators.append(env)
            // 把振荡器输出连到Bank的总输出
            osc >>> self.outputNode
        }
    }

    // 触发音符:设置基础音高、力度和ADSR参数
    func play(noteNumber: MIDINoteNumber, 
              velocity: MIDIVelocity, 
              attackDuration: Double = 0.1, 
              decayDuration: Double = 0.2, 
              sustainLevel: Double = 0.5, 
              releaseDuration: Double = 0.3) {
        // 找到空闲的振荡器和包络(没有就复用第一个,简单处理)
        guard let (freeOsc, freeEnv) = findFreeVoice() else { return }

        // 计算当前音符的基础频率
        let baseFrequency = AKMIDIConverters.frequency(forNoteNumber: noteNumber)
        freeOsc.frequency = baseFrequency

        // 配置包络的ADSR参数
        freeEnv.attackDuration = attackDuration
        freeEnv.decayDuration = decayDuration
        freeEnv.sustainLevel = sustainLevel
        freeEnv.releaseDuration = releaseDuration

        // 启动包络和振荡器
        freeEnv.start()
        freeOsc.start()
    }

    // 释放音符:触发包络的release阶段,结束后停止振荡器
    func stop(noteNumber: MIDINoteNumber) {
        for (osc, env) in zip(oscillators, envelopeGenerators) {
            if osc.isPlaying {
                env.stop()
                // 等包络释放完成后再停止振荡器,避免突然截断
                DispatchQueue.main.asyncAfter(deadline: .now() + env.releaseDuration) {
                    osc.stop()
                }
            }
        }
    }

    // 辅助方法:找到第一个空闲的振荡器+包络组合
    private func findFreeVoice() -> (AKSineOscillator, AKEnvelopeGenerator)? {
        for (osc, env) in zip(oscillators, envelopeGenerators) {
            if !osc.isPlaying {
                return (osc, env)
            }
        }
        return (oscillators[0], envelopeGenerators[0])
    }
}
关键细节说明
  1. 包络到频率的映射:
    上面的代码是把包络的0-1输出直接转成Hz偏移,如果你想要更符合音乐逻辑的音高变化(比如半音偏移),可以把映射逻辑改成:

    // 把包络值映射到-12到+12半音的偏移,再转成频率倍数
    env.output.map { 
        let semitoneOffset = $0 * 24 - 12 // 范围-12到+12
        return pow(2, semitoneOffset / 12) 
    } >>> osc.frequency
    

    这样包络输出0对应降12半音,1对应升12半音,sustain阶段保持在对应比例的音高。

  2. 扩展到其他参数:
    如果你想把包络同时连到多个参数(比如频率+滤波器截止频率),只需要多加几条连接就行:

    // 同时控制频率偏移和滤波器截止频率
    env.output.map { $0 * maxFrequencyOffset } >>> osc.frequency
    env.output.map { $0 * 5000 + 100 } >>> filter.cutoffFrequency
    
  3. 参考官方Bank源码:
    你可以去看AudioKit里AKOscillatorBank的源码,它的ADSR就是连到AKAmplitudeEnvelope再控制增益的,照着那个结构改参数目标就行,官方的代码在复音管理、音符匹配上更严谨,适合进阶优化。

内容的提问来源于stack exchange,提问作者headkit

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.20 07:21:36