// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. using System; using System.Globalization; using System.Threading; using UnityEngine; using HoloToolkit.Unity; using HoloToolkit.Unity.InputModule; namespace HoloToolkit.Sharing.VoiceChat { /// <summary> /// Transmits data from your microphone to other clients connected to a SessionServer. Requires any receiving client to be running the MicrophoneReceiver script. /// </summary> [RequireComponent(typeof(AudioSource))] public class MicrophoneTransmitter : MonoBehaviour { /// <summary> /// Which type of microphone/quality to access /// </summary> public MicStream.StreamCategory Streamtype = MicStream.StreamCategory.HIGH_QUALITY_VOICE; /// <summary> /// You can boost volume here as desired. 1 is default but probably too quiet. You can change during operation. /// </summary> public float InputGain = 2; /// <summary> /// Whether or not to send the microphone data across the network /// </summary> public bool ShouldTransmitAudio = true; /// <summary> /// Whether other users should be able to hear the transmitted audio /// </summary> public bool Mute; public Transform GlobalAnchorTransform; public bool ShowInterPacketTime; private DateTime timeOfLastPacketSend; private float worstTimeBetweenPackets; private int sequenceNumber; private int sampleRateType = 3; // 48000Hz private AudioSource audioSource; private bool hasServerConnection; private bool micStarted; public const int AudioPacketSize = 960; private CircularBuffer micBuffer = new CircularBuffer(AudioPacketSize * 10 * 2 * 4, true); private byte[] packetSamples = new byte[AudioPacketSize * 4]; // bit packers private readonly BitManipulator versionPacker = new BitManipulator(0x7, 0); // 3 bits, 0 shift private readonly BitManipulator audioStreamCountPacker = new BitManipulator(0x38, 3); // 3 bits, 3 shift private readonly BitManipulator channelCountPacker = new BitManipulator(0x1c0, 6); // 3 bits, 6 shift private readonly BitManipulator sampleRatePacker = new BitManipulator(0x600, 9); // 2 bits, 9 shift private readonly BitManipulator sampleTypePacker = new BitManipulator(0x1800, 11); // 2 bits, 11 shift private readonly BitManipulator sampleCountPacker = new BitManipulator(0x7fe000, 13); // 10 bits, 13 shift private readonly BitManipulator codecTypePacker = new BitManipulator(0x1800000, 23); // 2 bits, 23 shift private readonly BitManipulator mutePacker = new BitManipulator(0x2000000, 25); // 1 bits, 25 shift private readonly BitManipulator sequenceNumberPacker = new BitManipulator(0x7C000000, 26); // 6 bits, 26 shift private readonly Mutex audioDataMutex = new Mutex(); #region DebugVariables public bool HearSelf; private readonly CircularBuffer testCircularBuffer = new CircularBuffer(48000 * 2 * 4 * 3, true); private AudioSource testSource; public AudioClip TestClip; public bool SaveTestClip; #endregion private NetworkConnection GetActiveConnection() { NetworkConnection connection = null; var stage = SharingStage.Instance; if (stage && stage.Manager != null) { connection = stage.Manager.GetServerConnection(); } if (connection == null || !connection.IsConnected()) { return null; } return connection; } private void Awake() { audioSource = GetComponent<AudioSource>(); int errorCode = MicStream.MicInitializeCustomRate((int)Streamtype, AudioSettings.outputSampleRate); CheckForErrorOnCall(errorCode); if (errorCode == 0 || errorCode == (int)MicStream.ErrorCodes.ALREADY_RUNNING) { if (CheckForErrorOnCall(MicStream.MicSetGain(InputGain))) { audioSource.volume = HearSelf ? 1.0f : 0.0f; micStarted = CheckForErrorOnCall(MicStream.MicStartStream(false, false)); } } } private void OnAudioFilterRead(float[] buffer, int numChannels) { try { audioDataMutex.WaitOne(); if (micStarted && hasServerConnection) { if (CheckForErrorOnCall(MicStream.MicGetFrame(buffer, buffer.Length, numChannels))) { int dataSize = buffer.Length * 4; if (micBuffer.Write(buffer, 0, dataSize) != dataSize) { Debug.LogError("Send buffer filled up. Some audio will be lost."); } } } } catch (Exception e) { Debug.LogError(e.Message); } finally { audioDataMutex.ReleaseMutex(); } } private void Update() { CheckForErrorOnCall(MicStream.MicSetGain(InputGain)); audioSource.volume = HearSelf ? 1.0f : 0.0f; try { audioDataMutex.WaitOne(); var connection = GetActiveConnection(); hasServerConnection = (connection != null); if (hasServerConnection) { while (micBuffer.UsedCapacity >= 4 * AudioPacketSize) { TransmitAudio(connection); } } } catch (Exception e) { Debug.LogError(e.Message); } finally { audioDataMutex.ReleaseMutex(); } #region DebugInfo if (SaveTestClip && testCircularBuffer.UsedCapacity == testCircularBuffer.TotalCapacity) { float[] testBuffer = new float[testCircularBuffer.UsedCapacity / 4]; testCircularBuffer.Read(testBuffer, 0, testBuffer.Length * 4); testCircularBuffer.Reset(); TestClip = AudioClip.Create("testclip", testBuffer.Length / 2, 2, 48000, false); TestClip.SetData(testBuffer, 0); if (!testSource) { GameObject testObj = new GameObject("testclip"); testObj.transform.parent = transform; testSource = testObj.AddComponent<AudioSource>(); } testSource.PlayClip(TestClip); SaveTestClip = false; } #endregion } private void TransmitAudio(NetworkConnection connection) { micBuffer.Read(packetSamples, 0, 4 * AudioPacketSize); SendFixedSizedChunk(connection, packetSamples, packetSamples.Length); if (SaveTestClip) { testCircularBuffer.Write(packetSamples, 0, packetSamples.Length); } } private void SendFixedSizedChunk(NetworkConnection connection, byte[] data, int dataSize) { DateTime currentTime = DateTime.Now; float seconds = (float)(currentTime - timeOfLastPacketSend).TotalSeconds; timeOfLastPacketSend = currentTime; if (seconds < 10.0) { if (worstTimeBetweenPackets < seconds) { worstTimeBetweenPackets = seconds; } if (ShowInterPacketTime) { Debug.LogFormat("Microphone: Milliseconds since last sent: {0}, Worst: {1}", (seconds * 1000.0).ToString(CultureInfo.InvariantCulture), (worstTimeBetweenPackets * 1000.0).ToString(CultureInfo.InvariantCulture)); } } int clientId = SharingStage.Instance.Manager.GetLocalUser().GetID(); // pack the header NetworkOutMessage msg = connection.CreateMessage((byte)MessageID.AudioSamples); int dataCountFloats = dataSize / 4; msg.Write((byte)5); // 8 byte header size Int32 pack = 0; versionPacker.SetBits(ref pack, 1); // version audioStreamCountPacker.SetBits(ref pack, 1); // AudioStreamCount channelCountPacker.SetBits(ref pack, 1); // ChannelCount sampleRatePacker.SetBits(ref pack, sampleRateType); // SampleRate: 1 = 16000, 3 = 48000 sampleTypePacker.SetBits(ref pack, 0); // SampleType sampleCountPacker.SetBits(ref pack, dataCountFloats); // SampleCount (data count is in bytes and the actual data is in floats, so div by 4) codecTypePacker.SetBits(ref pack, 0); // CodecType mutePacker.SetBits(ref pack, Mute ? 1 : 0); sequenceNumberPacker.SetBits(ref pack, sequenceNumber++); sequenceNumber %= 32; msg.Write(pack); // the packed bits // This is where stream data starts. Write all data for one stream msg.Write(0.0f); // average amplitude. Not needed in direction from client to server. msg.Write(clientId); // non-zero client ID for this client. // HRTF position bits Vector3 cameraPosRelativeToGlobalAnchor = Vector3.zero; Vector3 cameraDirectionRelativeToGlobalAnchor = Vector3.zero; if (GlobalAnchorTransform != null) { cameraPosRelativeToGlobalAnchor = MathUtils.TransformPointFromTo( null, GlobalAnchorTransform, CameraCache.Main.transform.position); cameraDirectionRelativeToGlobalAnchor = MathUtils.TransformDirectionFromTo( null, GlobalAnchorTransform, CameraCache.Main.transform.position); } cameraPosRelativeToGlobalAnchor.Normalize(); cameraDirectionRelativeToGlobalAnchor.Normalize(); // Camera position msg.Write(cameraPosRelativeToGlobalAnchor.x); msg.Write(cameraPosRelativeToGlobalAnchor.y); msg.Write(cameraPosRelativeToGlobalAnchor.z); // HRTF direction bits msg.Write(cameraDirectionRelativeToGlobalAnchor.x); msg.Write(cameraDirectionRelativeToGlobalAnchor.y); msg.Write(cameraDirectionRelativeToGlobalAnchor.z); msg.WriteArray(data, (uint)dataCountFloats * 4); connection.Send(msg, MessagePriority.Immediate, MessageReliability.ReliableOrdered, MessageChannel.Audio, true); } private void OnDestroy() { CheckForErrorOnCall(MicStream.MicDestroy()); } private bool CheckForErrorOnCall(int returnCode) { return MicStream.CheckForErrorOnCall(returnCode); } #if DOTNET_FX // on device, deal with all the ways that we could suspend our program in as few lines as possible private void OnApplicationPause(bool pause) { if (pause) { CheckForErrorOnCall(MicStream.MicPause()); } else { CheckForErrorOnCall(MicStream.MicResume()); } } private void OnApplicationFocus(bool focused) { OnApplicationPause(!focused); } private void OnDisable() { OnApplicationPause(true); } private void OnEnable() { OnApplicationPause(false); } #endif } }