如何让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
相关产品推荐
相关产品推荐

