//******************************************************************************************************
// 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
}
}