/* * VideoKit * Copyright © 2025 Yusuf Olokoba. All Rights Reserved. */ namespace VideoKit { using System; using System.Linq; using System.Threading.Tasks; using UnityEngine; using Internal; /// /// VideoKit audio manager for streaming audio from audio devices. /// [Tooltip(@"VideoKit audio manager for streaming audio from audio devices.")] [HelpURL(@"https://videokit.ai/reference/videokitaudiomanager")] [DisallowMultipleComponent] public sealed class VideoKitAudioManager : MonoBehaviour { #region --Enumerations-- /// /// Audio sample rate. /// public enum SampleRate : int { /// /// Match Unity's audio DSP sample rate. /// MatchUnity = 0, /// /// 8KHz. /// _8000 = 8000, /// /// 16KHz. /// _16000 = 16000, /// /// 22.05KHz. /// _22050 = 22050, /// /// 24KHz. /// _24000 = 24000, /// /// 44.1KHz. /// _44100 = 44100, /// /// 48KHz. /// _48000 = 48000, } /// /// Audio channel count. /// public enum ChannelCount : int { /// /// Match Unity's audio DSP channel count. /// MatchUnity = 0, /// /// Mono audio. /// [InspectorName(@"Mono")] _1 = 1, /// /// Stereo audio. /// [InspectorName(@"Stereo")] _2 = 2, } #endregion #region --Inspector-- [Header(@"Configuration")] /// /// Configure the application audio session on awake. /// This only applies on iOS. /// [Tooltip(@"Configure the application audio session on awake. This only applies on iOS.")] public bool configureOnAwake = true; [Header(@"Format")] /// /// Audio sample rate. /// [Tooltip(@"Audio sample rate.")] public SampleRate sampleRate = SampleRate._44100; /// /// Audio channel count. /// [Tooltip(@"Audio channel count.")] public ChannelCount channelCount = ChannelCount._1; /// /// Request echo cancellation if the device supports it. /// [Tooltip(@"Request echo cancellation if the device supports it.")] public bool echoCancellation = false; #endregion #region --Client API-- /// /// Get or set the audio device used for streaming. /// public AudioDevice device { get => _device; set { // Switch mic without disposing output // We deliberately skip configuring the mic like we do in `StartRunning` if (running) { _device.StopRunning(); _device = value; _device?.StartRunning(OnSampleBuffer); } // Handle trivial case else _device = value; } } /// /// Whether the audio device is running. /// public bool running => _device?.running ?? false; /// /// Event raised when a new audio buffer is available. /// public event Action OnAudioBuffer; /// /// Start streaming audio. /// public async void StartRunning () => await StartRunningAsync(); /// /// Start streaming audio. /// public async Task StartRunningAsync () { // Check if (!isActiveAndEnabled) throw new InvalidOperationException(@"VideoKit: Audio manager failed to start running because component is disabled"); // Check if (running) return; // Request microphone permissions var permissions = await AudioDevice.CheckPermissions(request: true); if (permissions != MediaDevice.PermissionStatus.Authorized) throw new InvalidOperationException(@"VideoKit: User did not grant microphone permissions"); // Check device var devices = await AudioDevice.Discover(configureAudioSession: false); _device ??= devices.FirstOrDefault(); // configure once in `Awake` instead. if (_device == null) throw new InvalidOperationException(@"VideoKit: Audio manager failed to start running because no audio device is available"); // Configure microphone _device.sampleRate = sampleRate == SampleRate.MatchUnity ? AudioSettings.outputSampleRate : (int)sampleRate; _device.channelCount = channelCount == ChannelCount.MatchUnity ? (int)AudioSettings.speakerMode : (int)channelCount; _device.echoCancellation = echoCancellation; // devices can say they don't support AEC even when they do // Start running _device.StartRunning(OnSampleBuffer); } /// /// Stop streaming audio. /// public void StopRunning () { // Stop if (running) _device.StopRunning(); } #endregion #region --Operations-- private AudioDevice _device; private void Awake () { if (configureOnAwake) VideoKit.ConfigureAudioSession(); } private void OnSampleBuffer (AudioBuffer audioBuffer) => OnAudioBuffer?.Invoke(audioBuffer); private void OnDestroy () { if (running) StopRunning(); } #endregion } }