//****************************************************************************************************** // MicrophoneInputAdapter.cs - Gbtc // // Copyright © 2013, Grid Protection Alliance. All Rights Reserved. // // Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See // the NOTICE file distributed with this work for additional information regarding copyright ownership. // The GPA licenses this file to you under the MIT License (MIT), the "License"; you may // not use this file except in compliance with the License. You may obtain a copy of the License at: // // http://www.opensource.org/licenses/MIT // // Unless agreed to in writing, the subject software distributed under the License is distributed on an // "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the // License for the specific language governing permissions and limitations. // // Code Modification History: // ---------------------------------------------------------------------------------------------------- // 11/29/2013 - Stephen C. Wills // Generated original version of source code. // //****************************************************************************************************** using System; using System.Collections.Generic; using System.ComponentModel; using System.Text; using System.Threading; using GSF; using GSF.Diagnostics; using GSF.TimeSeries; using GSF.TimeSeries.Adapters; using NAudio.CoreAudioApi; using NAudio.Wave; namespace AudioAdapters { /// /// Represents an input adapter that reads measurements from a recording device. /// [Description("MIC: Reads measurements from a recording device")] public class MicrophoneInputAdapter : InputAdapterBase { #region [ Members ] // Fields private IWaveIn m_waveIn; private long m_ticksPerSample; private int m_sampleSize; private int m_channels; private TypeCode m_sampleTypeCode; private long m_lastSampleTime; private long m_samplesProcessed; private bool m_disposed; #endregion #region [ Properties ] /// /// Gets the flag indicating if this adapter supports temporal processing. /// public override bool SupportsTemporalProcessing => false; /// /// Gets flag that determines if the data input connects asynchronously. /// /// /// Derived classes should return true when data input source is connects asynchronously, otherwise return false. /// protected override bool UseAsyncConnect => false; /// /// Returns the detailed status of the data input source. /// /// /// Derived classes should extend status with implementation specific information. /// public override string Status { get { StringBuilder status = new StringBuilder(base.Status); status.AppendFormat(" Ticks Per Sample: {0}", m_ticksPerSample); status.AppendLine(); status.AppendFormat(" Sample Size: {0}", m_sampleSize); status.AppendLine(); status.AppendFormat(" Channels: {0}", m_channels); status.AppendLine(); return status.ToString(); } } #endregion #region [ Methods ] /// /// Gets a short one-line status of this . /// /// Maximum number of available characters for display. /// A short one-line summary of the current status of this . public override string GetShortStatus(int maxLength) { return $"{m_samplesProcessed} samples processed so far...".CenterText(maxLength); } /// /// Attempts to connect to data input source. /// /// /// Derived classes should attempt connection to data input source here. Any exceptions thrown /// by this implementation will result in restart of the connection cycle. /// protected override void AttemptConnection() { // ReSharper disable once UnusedVariable > Justification: implementation pattern disposes any existing object using (IWaveIn waveIn = m_waveIn) m_waveIn = new WasapiCapture(); m_ticksPerSample = Ticks.PerSecond / m_waveIn.WaveFormat.SampleRate; m_sampleSize = m_waveIn.WaveFormat.BitsPerSample / 8; m_channels = m_waveIn.WaveFormat.Channels; m_sampleTypeCode = GetSampleTypeCode(); Interlocked.Exchange(ref m_lastSampleTime, 0L); m_samplesProcessed = 0L; if (m_channels != OutputMeasurements.Length) OnStatusMessage(MessageLevel.Warning, "Number of output measurements does not match the number of channels."); m_waveIn.DataAvailable += WaveIn_DataAvailable; m_waveIn.RecordingStopped += WaveIn_RecordingStopped; m_waveIn.StartRecording(); } /// /// Attempts to disconnect from data input source. /// /// /// Derived classes should attempt disconnect from data input source here. Any exceptions thrown /// by this implementation will be reported to host via event. /// protected override void AttemptDisconnection() { if ((object)m_waveIn != null) { m_waveIn.Dispose(); m_waveIn = null; } } /// /// Releases the unmanaged resources used by the object and optionally releases the managed resources. /// /// true to release both managed and unmanaged resources; false to release only unmanaged resources. protected override void Dispose(bool disposing) { if (!m_disposed) { try { if (disposing) { if ((object)m_waveIn != null) { m_waveIn.Dispose(); m_waveIn = null; } } } finally { m_disposed = true; // Prevent duplicate dispose. base.Dispose(disposing); // Call base class Dispose(). } } } private TypeCode GetSampleTypeCode() { // Determine sample data type based on bit size and audio format switch (m_waveIn.WaveFormat.BitsPerSample) { case 8: return TypeCode.Byte; case 16: return TypeCode.Int16; case 24: // .NET does not define an Int24 type code and since an Int24 will // fit inside an Int32, the Int32 type code is returned. return TypeCode.Int32; case 32: if (m_waveIn.WaveFormat.Encoding == WaveFormatEncoding.IeeeFloat) return TypeCode.Single; return TypeCode.Int32; case 64: if (m_waveIn.WaveFormat.Encoding == WaveFormatEncoding.IeeeFloat) return TypeCode.Double; return TypeCode.Int64; default: // Unable to determine proper type code, consumer may be using a special data format... return TypeCode.Empty; } } private short ConvertToPCM16(double sample) { switch (m_waveIn.WaveFormat.BitsPerSample) { case 8: return (short)((sample - 128.0D) / sbyte.MaxValue * short.MaxValue); case 16: return (short)sample; case 24: return (short)((double)sample / Int24.MaxValue * short.MaxValue); case 32: if (m_waveIn.WaveFormat.Encoding == WaveFormatEncoding.IeeeFloat) return (short)(sample * short.MaxValue); return (short)((double)sample / int.MaxValue * short.MaxValue); case 64: if (m_waveIn.WaveFormat.Encoding == WaveFormatEncoding.IeeeFloat) return (short)(sample * short.MaxValue); return (short)((double)sample / long.MaxValue * short.MaxValue); default: throw new InvalidOperationException($"Cannot convert sample \'{sample}\' into 16-bits."); } } private void WaveIn_DataAvailable(object sender, WaveInEventArgs waveInEventArgs) { int index = 0; int numSamples = waveInEventArgs.BytesRecorded / m_sampleSize / m_channels; List measurements = new List(); long sampleTime; LittleBinaryValue sampleValue; // Get the timestamp for the first recorded sample if (Interlocked.Read(ref m_lastSampleTime) == 0L) Interlocked.Exchange(ref m_lastSampleTime, DateTime.UtcNow.Ticks - (m_ticksPerSample * numSamples)); // Get the timestamp for the first sample in this block of data sampleTime = Interlocked.Read(ref m_lastSampleTime) + m_ticksPerSample; // Parse each recorded sample in this block of data for (int sampleIndex = 0; sampleIndex < numSamples; sampleIndex++) { // Parse one value per channel per sample for (int channelIndex = 0; channelIndex < m_channels; channelIndex++) { if (channelIndex < OutputMeasurements.Length) { // Create a measurement for this value sampleValue = new LittleBinaryValue(m_sampleTypeCode, waveInEventArgs.Buffer, index + (channelIndex * m_sampleSize), m_sampleSize); measurements.Add(Measurement.Clone(OutputMeasurements[channelIndex], ConvertToPCM16(sampleValue.ConvertToType(TypeCode.Double).ToDouble()), sampleTime)); } } // Update the last sample time Interlocked.Exchange(ref m_lastSampleTime, sampleTime); // Get the timestamp for the next sample in this block of data sampleTime += m_ticksPerSample; // Update the index to the next sample in this block of data index += m_sampleSize * m_channels; } // Publish streaming microphone data OnNewMeasurements(measurements); m_samplesProcessed += numSamples; } private void WaveIn_RecordingStopped(object sender, StoppedEventArgs stoppedEventArgs) { if ((object)stoppedEventArgs.Exception != null) { string errorMessage = $"An error occurred during recording: {stoppedEventArgs.Exception.Message}"; OnProcessException(MessageLevel.Warning, new InvalidOperationException(errorMessage, stoppedEventArgs.Exception)); } } #endregion } }