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

如何让Wwise音频输入插件捕获Unity主音频输出而非单个AudioSource.clip?

实现Unity主音频总线/多AudioClip向Wwise音频输入插件传输音频

需求说明

现有Unity项目使用Wwise音频输入插件,当前AkMicrophone脚本仅支持通过单个AudioSource.clip向Wwise传输音频。需要实现以下任一目标:

  • 将Unity主音频总线的所有输出(立体声左右声道)发送至Wwise,替代单个音频片段的传输
  • 支持同时向Wwise音频输入插件传输多个AudioClip

原AkMicrophone脚本代码:

using System.Collections;
using UnityEngine;
using UnityEngine.Events;

[UnityEngine.RequireComponent(typeof(UnityEngine.AudioSource))]
public class AkMicrophone : UnityEngine.MonoBehaviour
{
    public AK.Wwise.Event MicrophoneEvent;
    public AK.Wwise.RTPC MicrophoneLevelRTPC;
    public int SampleRate = 48000;
    public int InitialReadDelayInSamples = 4096;
    [UnityEngine.Header("Invoked on exceeding -48dB")]
    public UnityEvent OnMicrophoneAction;

    public static AkMicrophone Instance
    {
        get;
        private set;
    }

    public bool IsAboveThreshold
    {
        get
        {
            bool Above = currentMicLevel > ATTACK_LEVEL_THRESHOLD;
            currentMicLevel = -48;
            return Above;
        }
    }

    // Buffering consts
    private const int NUMBER_OF_CHANNELS = 1;
    private const int BUFFER_SIZE_IN_SECONDS = 2;
    private int BufferSizeInSamples { get { return BUFFER_SIZE_IN_SECONDS * NUMBER_OF_CHANNELS * SampleRate; } }

    // Level monitoring
    private float ATTACK_LEVEL_THRESHOLD = -24.0f;
    private float currentMicLevel = -48;
    
    // Unity Microphone input handling
    private UnityEngine.AudioSource MicrophoneSource;
    private int ReadPosition = 0;
    public bool IsPlaying { get { return m_PlayingID != AkSoundEngine.AK_INVALID_PLAYING_ID; } }
    private float[] SamplesBuffer = null;
    private object BufferLock = new object();

    private uint m_PlayingID = AkSoundEngine.AK_INVALID_PLAYING_ID;

    void AudioFormatDelegate(uint playingID, AkAudioFormat audioFormat)
    {
        audioFormat.channelConfig.uNumChannels = NUMBER_OF_CHANNELS;
        audioFormat.uSampleRate = (uint)SampleRate;
    }

    public int samplePerFrame;
    bool AudioSamplesDelegate(uint playingID, uint channelIndex, float[] samples)
    {
        if (IsPlaying)
        {
            samplePerFrame = samples.Length;
            Debug.Log("   delegate samples.Length " + samples.Length);

            try
            {
                lock (BufferLock)
                {
                    System.Array.Copy(SamplesBuffer, 0, samples, 0, samples.Length);
                    ReadPosition = (ReadPosition + samples.Length) % BufferSizeInSamples;
                }
            }
            catch (System.Exception e)
            {
                UnityEngine.Debug.LogError("Wwise Audio Input, exception occured: " + e.ToString());
                return false;
            }
        }

        // Return false to indicate that there is no more data to provide. This will also stop the associated event.
        return IsPlaying;
    }

    public bool checkBufferEnd;

    public void playOneShot()
    {
        StopSound();
        ReadPosition = 0;
        m_PlayingID = AkAudioInputManager.PostAudioInputEvent(MicrophoneEvent.Id, gameObject, AudioSamplesDelegate, AudioFormatDelegate);
        checkBufferEnd = true;
        playedOnce = false;
    }

    public bool playedOnce = false;
    bool checkOneShotEnded()
    {
        if (checkBufferEnd)
        {
            if (ReadPosition >= SamplesBuffer.Length - samplePerFrame)
            {
                return true;
            }
            return false;
        }
        return false;
    }
    
    void GetMicrophoneSamples()
    {
        if (MicrophoneSource != null && MicrophoneSource.clip != null)
        {
            Debug.Log("  get mic sample ReadPosition : " + ReadPosition + "  length" + MicrophoneSource.clip.length +
             " buffersize: " + SamplesBuffer.Length  + "   BufferSizeInSamples: " + BufferSizeInSamples);

            if (checkBufferEnd)
            {
                if (ReadPosition >= SamplesBuffer.Length - samplePerFrame)
                {
                    playedOnce = true;
                    StopSound();
                }
                if(playedOnce)
                    return;
            }

            lock (BufferLock)
            {
                if(!checkOneShotEnded())
                    MicrophoneSource.clip.GetData(SamplesBuffer, ReadPosition);
            }
        }
    }

    void Update()
    {
        GetMicrophoneSamples();

        if (IsPlaying)
        {
            int RTPCValue = (int)AkQueryRTPCValue.RTPCValue_GameObject;
            AkSoundEngine.GetRTPCValue((uint)MicrophoneLevelRTPC.Id, gameObject, m_PlayingID, out currentMicLevel, ref RTPCValue);
            if (IsAboveThreshold)
                OnMicrophoneAction.Invoke();
        }
    }

    IEnumerator Start()
    {
        if (Instance != null)
        {
            UnityEngine.Debug.LogError("Wwise Microphone input: Attempted to add a second AkMicrophone component.");
            yield break;
        }
        Instance = this;

        yield return null;

        SamplesBuffer = new float[BufferSizeInSamples];
        StartSound();
    }

    public void StartSound()
    {
        if (!IsPlaying)
        {
            MicrophoneSource = GetComponent<UnityEngine.AudioSource>();
            MicrophoneSource.clip = UnityEngine.Microphone.Start(null, true, BUFFER_SIZE_IN_SECONDS, SampleRate);
            GetMicrophoneSamples();

            // Since GetData gives a LOT of data, move our ReadPosition closer to the current samples upon first read
            ReadPosition = UnityEngine.Microphone.GetPosition(null) - InitialReadDelayInSamples;
            ReadPosition = (ReadPosition + BufferSizeInSamples) % BufferSizeInSamples;

            Debug.Log(" ReadPosition " + ReadPosition);

            m_PlayingID = AkAudioInputManager.PostAudioInputEvent(MicrophoneEvent.Id, gameObject, AudioSamplesDelegate, AudioFormatDelegate);
        }
    }
    public void StopSound()
    {
        m_PlayingID = AkSoundEngine.AK_INVALID_PLAYING_ID;
        MicrophoneEvent.Stop(gameObject);
    }

    private void OnDestroy()
    {
        StopSound();
    }
}

方案一:捕获Unity主音频总线输出到Wwise

该方案通过AudioListener.GetOutputData获取Unity主音频总线的立体声数据,替代原有的麦克风采样逻辑,实现将所有Unity音频输出发送至Wwise。

修改后的脚本

using System.Collections;
using UnityEngine;
using UnityEngine.Events;

public class AkUnityMainBusInput : MonoBehaviour
{
    public AK.Wwise.Event AudioInputEvent;
    public AK.Wwise.RTPC AudioLevelRTPC;
    public int SampleRate = 48000;
    [UnityEngine.Header("Invoked on exceeding -48dB")]
    public UnityEvent OnAudioThresholdExceeded;

    public static AkUnityMainBusInput Instance { get; private set; }

    public bool IsAboveThreshold
    {
        get
        {
            bool above = currentAudioLevel > ATTACK_LEVEL_THRESHOLD;
            currentAudioLevel = -48;
            return above;
        }
    }

    // 立体声通道
    private const int NUMBER_OF_CHANNELS = 2;
    private const int BUFFER_SIZE_IN_SECONDS = 2;
    private int BufferSizeInSamples => BUFFER_SIZE_IN_SECONDS * NUMBER_OF_CHANNELS * SampleRate;

    private float ATTACK_LEVEL_THRESHOLD = -24.0f;
    private float currentAudioLevel = -48;
    
    private float[] SamplesBuffer = null;
    private object BufferLock = new object();
    private uint m_PlayingID = AkSoundEngine.AK_INVALID_PLAYING_ID;
    public bool IsPlaying => m_PlayingID != AkSoundEngine.AK_INVALID_PLAYING_ID;

    void AudioFormatDelegate(uint playingID, AkAudioFormat audioFormat)
    {
        audioFormat.channelConfig.uNumChannels = NUMBER_OF_CHANNELS;
        audioFormat.uSampleRate = (uint)SampleRate;
    }

    bool AudioSamplesDelegate(uint playingID, uint channelIndex, float[] samples)
    {
        if (!IsPlaying) return false;

        try
        {
            lock (BufferLock)
            {
                System.Array.Copy(SamplesBuffer, 0, samples, 0, samples.Length);
            }
        }
        catch (System.Exception e)
        {
            Debug.LogError("Wwise Audio Input Exception: " + e.ToString());
            return false;
        }

        return IsPlaying;
    }

    void CaptureMainBusSamples()
    {
        if (!IsPlaying) return;

        lock (BufferLock)
        {
            // 获取主音频总线的立体声数据
            AudioListener.GetOutputData(SamplesBuffer, 0);
        }
    }

    void Update()
    {
        CaptureMainBusSamples();

        if (IsPlaying)
        {
            int rtpcValueType = (int)AkQueryRTPCValue.RTPCValue_GameObject;
            AkSoundEngine.GetRTPCValue((uint)AudioLevelRTPC.Id, gameObject, m_PlayingID, out currentAudioLevel, ref rtpcValueType);
            if (IsAboveThreshold)
                OnAudioThresholdExceeded.Invoke();
        }
    }

    IEnumerator Start()
    {
        if (Instance != null)
        {
            Debug.LogError("Multiple AkUnityMainBusInput instances are not allowed.");
            yield break;
        }
        Instance = this;

        // 等待音频系统初始化
        yield return new WaitForSeconds(0.1f);

        SamplesBuffer = new float[BufferSizeInSamples];
        StartAudioInput();
    }

    public void StartAudioInput()
    {
        if (IsPlaying) return;

        m_PlayingID = AkAudioInputManager.PostAudioInputEvent(AudioInputEvent.Id, gameObject, AudioSamplesDelegate, AudioFormatDelegate);
    }

    public void StopAudioInput()
    {
        if (!IsPlaying) return;

        m_PlayingID = AkSoundEngine.AK_INVALID_PLAYING_ID;
        AudioInputEvent.Stop(gameObject);
    }

    private void OnDestroy()
    {
        StopAudioInput();
    }
}

注意事项

  • 移除了原有的AudioSource依赖,无需挂载AudioSource组件
  • 通道数改为2,适配立体声输出
  • 使用AudioListener.GetOutputData在每一帧捕获主总线音频数据,该方法仅在主线程调用有效
  • 确保Wwise工程中的输入插件设置与脚本的采样率、通道数匹配

方案二:支持多AudioClip同时传输到Wwise

该方案维护一个AudioClip列表,将多个Clip的采样数据混合后发送至Wwise,支持同时传输多个音频片段。

修改后的脚本

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;

[RequireComponent(typeof(AudioSource))]
public class AkMultiClipInput : MonoBehaviour
{
    public AK.Wwise.Event AudioInputEvent;
    public AK.Wwise.RTPC AudioLevelRTPC;
    public int SampleRate = 48000;
    public int InitialReadDelayInSamples = 4096;
    [UnityEngine.Header("Invoked on exceeding -48dB")]
    public UnityEvent OnAudioThresholdExceeded;

    public static AkMultiClipInput Instance { get; private set; }

    public bool IsAboveThreshold
    {
        get
        {
            bool above = currentAudioLevel > ATTACK_LEVEL_THRESHOLD;
            currentAudioLevel = -48;
            return above;
        }
    }

    // 单声道适配,如需立体声可改为2
    private const int NUMBER_OF_CHANNELS = 1;
    private const int BUFFER_SIZE_IN_SECONDS = 2;
    private int BufferSizeInSamples => BUFFER_SIZE_IN_SECONDS * NUMBER_OF_CHANNELS * SampleRate;

    private float ATTACK_LEVEL_THRESHOLD = -24.0f;
    private float currentAudioLevel = -48;
    
    private AudioSource _audioSource;
    private float[] SamplesBuffer = null;
    private object BufferLock = new object();
    private uint m_PlayingID = AkSoundEngine.AK_INVALID_PLAYING_ID;
    public bool IsPlaying => m_PlayingID != AkSoundEngine.AK_INVALID_PLAYING_ID;

    // 待传输的AudioClip列表
    public List<AudioClip> ClipsToTransmit = new List<AudioClip>();
    // 每个Clip的当前读取位置
    private Dictionary<AudioClip, int> _clipReadPositions = new Dictionary<AudioClip, int>();

    void AudioFormatDelegate(uint playingID, AkAudioFormat audioFormat)
    {
        audioFormat.channelConfig.uNumChannels = NUMBER_OF_CHANNELS;
        audioFormat.uSampleRate = (uint)SampleRate;
    }

    bool AudioSamplesDelegate(uint playingID, uint channelIndex, float[] samples)
    {
        if (!IsPlaying) return false;

        try
        {
            lock (BufferLock)
            {
                System.Array.Copy(SamplesBuffer, 0, samples, 0, samples.Length);
            }
        }
        catch (System.Exception e)
        {
            Debug.LogError("Wwise Audio Input Exception: " + e.ToString());
            return false;
        }

        return IsPlaying;
    }

    void MixClipSamples()
    {
        if (!IsPlaying || ClipsToTransmit.Count == 0) return;

        lock (BufferLock)
        {
            // 清空缓冲区
            System.Array.Clear(SamplesBuffer, 0, SamplesBuffer.Length);

            foreach (var clip in ClipsToTransmit)
            {
                if (!_clipReadPositions.ContainsKey(clip))
                    _clipReadPositions[clip] = 0;

                int readPos = _clipReadPositions[clip];
                float[] clipSamples = new float[SamplesBuffer.Length];
                
                // 获取当前Clip的采样数据
                clip.GetData(clipSamples, readPos);

                // 混合到主缓冲区
                for (int i = 0; i < SamplesBuffer.Length; i++)
                {
                    SamplesBuffer[i] += clipSamples[i];
                    // 防止音量过载,进行简单限幅
                    SamplesBuffer[i] = Mathf.Clamp(SamplesBuffer[i], -1f, 1f);
                }

                // 更新读取位置,循环播放Clip
                _clipReadPositions[clip] = (readPos + SamplesBuffer.Length) % (clip.samples * clip.channels);
            }
        }
    }

    void Update()
    {
        MixClipSamples();

        if (IsPlaying)
        {
            int rtpcValueType = (int)AkQueryRTPCValue.RTPCValue_GameObject;
            AkSoundEngine.GetRTPCValue((uint)AudioLevelRTPC.Id, gameObject, m_PlayingID, out currentAudioLevel, ref rtpcValueType);
            if (IsAboveThreshold)
                OnAudioThresholdExceeded.Invoke();
        }
    }

    IEnumerator Start()
    {
        if (Instance != null)
        {
            Debug.LogError("Multiple AkMultiClipInput instances are not allowed.");
            yield break;
        }
        Instance = this;

        _audioSource = GetComponent<AudioSource>();
        yield return null;

        SamplesBuffer = new float[BufferSizeInSamples];
        StartAudioInput();
    }

    public void StartAudioInput()
    {
        if (IsPlaying) return;

        m_PlayingID = AkAudioInputManager.PostAudioInputEvent(AudioInputEvent.Id, gameObject, AudioSamplesDelegate, AudioFormatDelegate);
    }

    public void StopAudioInput()
    {
        if (!IsPlaying) return;

        m_PlayingID = AkSoundEngine.AK_INVALID_PLAYING_ID;
        AudioInputEvent.Stop(gameObject);
        _clipReadPositions.Clear();
    }

    // 添加Clip到传输列表
    public void AddClipToTransmit(AudioClip clip)
    {
        if (!ClipsToTransmit.Contains(clip))
        {
            ClipsToTransmit.Add(clip);
            _clipReadPositions[clip] = 0;
        }
    }

    // 从传输列表移除Clip
    public void RemoveClipFromTransmit(AudioClip clip)
    {
        ClipsToTransmit.Remove(clip);
        _clipReadPositions.Remove(clip);
    }

    private void OnDestroy()
    {
        StopAudioInput();
    }
}

注意事项

  • 通过ClipsToTransmit列表管理需要传输的音频片段,可通过AddClipToTransmit/RemoveClipFromTransmit动态添加/移除
  • 实现了多Clip采样数据的混合,并做了限幅处理防止音量过载
  • 若需要立体声传输,可将NUMBER_OF_CHANNELS改为2,并确保所有Clip的通道数一致
  • 确保Wwise工程的输入插件设置与脚本的采样率、通道数匹配

内容的提问来源于stack exchange

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.04 15:00:43